From 90acfcbcdc4d8dccd2d5261835a2297f4d6c739d Mon Sep 17 00:00:00 2001 From: Dmytro Date: Tue, 27 Aug 2024 13:49:30 +0100 Subject: [PATCH 1/5] Add category chips, add loader for ecosystem, fix trending tab --- src/App.scss | 20 ++++++++++++-------- src/components/SchainDetails.tsx | 2 +- src/components/chains/HubApps.tsx | 2 +- src/components/ecosystem/AllApps.tsx | 10 +++++++++- src/components/ecosystem/AppCardV2.tsx | 4 ++-- src/components/ecosystem/CategoriesChips.tsx | 19 +++++++------------ src/components/ecosystem/TrendingApps.tsx | 2 ++ src/core/ecosystem/apps.ts | 8 ++++++++ src/pages/App.tsx | 10 +++++----- src/pages/Ecosystem.tsx | 3 +++ src/styles/chip.scss | 2 +- 11 files changed, 51 insertions(+), 31 deletions(-) diff --git a/src/App.scss b/src/App.scss index 3ba7bea..273224d 100644 --- a/src/App.scss +++ b/src/App.scss @@ -15,7 +15,6 @@ body { -moz-osx-font-smoothing: grayscale; } - .bridge { min-height: 100vh; min-width: 100vw; @@ -139,11 +138,14 @@ body { height: 85vh; padding: 20px !important; display: flex; - /* relevant part */ flex-direction: column; - /* relevant part */ overflow: hidden; - /* relevant part */ +} + +// todo: move to cmn +.flex-w { + display: flex; + flex-wrap: wrap; } @media only screen and (min-device-width: 320px) and (max-device-width: 480px) { @@ -156,17 +158,13 @@ body { .br__modalInner { max-height: calc(90vh - 250px); display: flex; - /* relevant part */ flex-direction: column; - /* relevant part */ overflow: hidden; } .scrollable { display: flex; - /* relevant part */ flex-direction: column; - /* relevant part */ overflow: hidden; } @@ -457,6 +455,12 @@ body::-webkit-scrollbar { box-shadow: none !important; } +.favsBtn { + white-space: nowrap; + min-width: auto; + height: min-content; +} + .outlinedGray { background: rgb(126 126 126 / 15%); } diff --git a/src/components/SchainDetails.tsx b/src/components/SchainDetails.tsx index d17227a..fe375d9 100644 --- a/src/components/SchainDetails.tsx +++ b/src/components/SchainDetails.tsx @@ -190,7 +190,7 @@ export default function SchainDetails(props: {
- +
diff --git a/src/components/chains/HubApps.tsx b/src/components/chains/HubApps.tsx index aa0808b..1a99c9d 100644 --- a/src/components/chains/HubApps.tsx +++ b/src/components/chains/HubApps.tsx @@ -55,7 +55,7 @@ export default function HubApps(props: { } return ( - + {appCards} ) diff --git a/src/components/ecosystem/AllApps.tsx b/src/components/ecosystem/AllApps.tsx index 2379b90..a0d9a63 100644 --- a/src/components/ecosystem/AllApps.tsx +++ b/src/components/ecosystem/AllApps.tsx @@ -22,24 +22,32 @@ */ import React, { useMemo } from 'react' +import { cls, cmn } from '@skalenetwork/metaport' import { type types } from '@/core' + import { useLikedApps } from '../../LikedAppsContext' import AppCardV2 from './AppCardV2' import { Grid } from '@mui/material' import { isNewApp } from '../../core/ecosystem/utils' +import Loader from '../Loader' interface AllAppsProps { skaleNetwork: types.SkaleNetwork chainsMeta: types.ChainsMetadataMap apps: types.AppWithChainAndName[] newApps: types.AppWithTimestamp[] + loaded: boolean } -const AllApps: React.FC = ({ skaleNetwork, chainsMeta, apps, newApps }) => { +const AllApps: React.FC = ({ skaleNetwork, chainsMeta, apps, newApps, loaded }) => { const { getTrendingApps, getAppId } = useLikedApps() const trendingAppIds = useMemo(() => getTrendingApps(), [getTrendingApps]) + if (!loaded) return + if (apps.length === 0) + return

No apps found

+ return ( {apps.map((app: types.AppWithChainAndName) => { diff --git a/src/components/ecosystem/AppCardV2.tsx b/src/components/ecosystem/AppCardV2.tsx index 42db25f..726c4f7 100644 --- a/src/components/ecosystem/AppCardV2.tsx +++ b/src/components/ecosystem/AppCardV2.tsx @@ -31,7 +31,7 @@ import { getChainShortAlias } from '../../core/chain' import { chainBg, getChainAlias } from '../../core/metadata' import CollapsibleDescription from '../CollapsibleDescription' -import AppCategoriesChips from './CategoriesChips' +import CategoriesChips from './CategoriesChips' import SocialButtons from './Socials' import { ChipTrending, ChipNew, ChipPreTge } from '../Chip' @@ -86,7 +86,7 @@ export default function AppCard(props: { {appMeta.tags?.includes('pretge') && }
- + = ({ categories, className }) => { - const [expanded, setExpanded] = useState(false) - +const CategoriesChips: React.FC = ({ categories, all, className }) => { const chips = useMemo(() => { if (!categories) return [] @@ -81,18 +81,13 @@ const CategoriesChips: React.FC = ({ categories, className if (chips.length === 0) return null - const visibleChips = expanded ? chips : chips.slice(0, 2) + const visibleChips = all ? chips : chips.slice(0, 2) const remainingChips = chips.length - 2 return ( - + {visibleChips} - {remainingChips > 0 && ( - setExpanded(!expanded)} - /> - )} + {remainingChips > 0 && !all && } ) } diff --git a/src/components/ecosystem/TrendingApps.tsx b/src/components/ecosystem/TrendingApps.tsx index 4defcd3..a4037a1 100644 --- a/src/components/ecosystem/TrendingApps.tsx +++ b/src/components/ecosystem/TrendingApps.tsx @@ -29,6 +29,7 @@ import { Box, Grid } from '@mui/material' import { cls } from '@skalenetwork/metaport' import Carousel from '../Carousel' import { isNewApp } from '../../core/ecosystem/utils' +import { getAppMeta } from '../../core/ecosystem/apps' interface TrendingAppsProps { skaleNetwork: types.SkaleNetwork @@ -67,6 +68,7 @@ const TrendingApps: React.FC = ({ {trendingAppIds.map((appId) => { const { chain, app } = getAppInfoById(appId) const isNew = isNewApp({ chain, app }, newApps) + if (!getAppMeta(chainsMeta, chain, app)) return null return ( diff --git a/src/core/ecosystem/apps.ts b/src/core/ecosystem/apps.ts index 23e6efe..6be51a4 100644 --- a/src/core/ecosystem/apps.ts +++ b/src/core/ecosystem/apps.ts @@ -81,3 +81,11 @@ export function filterAppsBySearchTerm( getChainAlias(chainsMeta, app.chain).toLowerCase().includes(st) ) } + +export function getAppMeta( + chainsMeta: types.ChainsMetadataMap, + chain: string, + app: string +): types.AppMetadata | undefined { + return chainsMeta[chain]?.apps?.[app] +} diff --git a/src/pages/App.tsx b/src/pages/App.tsx index af12684..63a9ea7 100644 --- a/src/pages/App.tsx +++ b/src/pages/App.tsx @@ -65,7 +65,7 @@ import { chainBg, getChainAlias } from '../core/metadata' import { addressUrl, getExplorerUrl, getTotalAppCounters } from '../core/explorer' import { MAINNET_CHAIN_LOGOS, MAX_APPS_DEFAULT, OFFCHAIN_APP } from '../core/constants' import SocialButtons from '../components/ecosystem/Socials' -import AppCategoriesChips from '../components/ecosystem/CategoriesChips' +import CategoriesChips from '../components/ecosystem/CategoriesChips' import { useLikedApps } from '../LikedAppsContext' import { useAuth } from '../AuthContext' import ErrorTile from '../components/ErrorTile' @@ -217,19 +217,19 @@ export default function App(props: {
- +
- +
- +

{appAlias}

diff --git a/src/pages/Ecosystem.tsx b/src/pages/Ecosystem.tsx index 7d124e4..de7b3bf 100644 --- a/src/pages/Ecosystem.tsx +++ b/src/pages/Ecosystem.tsx @@ -71,6 +71,7 @@ export default function Ecosystem(props: { const [filteredApps, setFilteredApps] = useState([]) const [searchTerm, setSearchTerm] = useState('') const [activeTab, setActiveTab] = useState(0) + const [loaded, setLoaded] = useState(false) const newApps = useMemo( () => getRecentApps(props.chainsMeta, MAX_APPS_DEFAULT), @@ -91,6 +92,7 @@ export default function Ecosystem(props: { props.chainsMeta ) setFilteredApps(filtered) + setLoaded(true) }, [allApps, checkedItems, searchTerm]) const handleSetCheckedItems = (newCheckedItems: string[]) => { @@ -193,6 +195,7 @@ export default function Ecosystem(props: { skaleNetwork={props.mpc.config.skaleNetwork} chainsMeta={props.chainsMeta} newApps={newApps} + loaded={loaded} /> )} {activeTab === 1 && ( diff --git a/src/styles/chip.scss b/src/styles/chip.scss index 21433da..036e034 100644 --- a/src/styles/chip.scss +++ b/src/styles/chip.scss @@ -3,7 +3,7 @@ overflow-x: auto; white-space: nowrap; gap: 8px; - border-radius: 25px; + // border-radius: 25px; position: relative; } From fbfba4d791247f57b91199b9ae9f0f61fce39cd8 Mon Sep 17 00:00:00 2001 From: Dmytro Date: Tue, 27 Aug 2024 15:57:15 +0100 Subject: [PATCH 2/5] Fix categories filter UI --- src/App.scss | 1 + src/components/ecosystem/SelectedCategories.tsx | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/App.scss b/src/App.scss index 273224d..56ac46e 100644 --- a/src/App.scss +++ b/src/App.scss @@ -146,6 +146,7 @@ body { .flex-w { display: flex; flex-wrap: wrap; + gap: 10px; } @media only screen and (min-device-width: 320px) and (max-device-width: 480px) { diff --git a/src/components/ecosystem/SelectedCategories.tsx b/src/components/ecosystem/SelectedCategories.tsx index 2e17688..0898531 100644 --- a/src/components/ecosystem/SelectedCategories.tsx +++ b/src/components/ecosystem/SelectedCategories.tsx @@ -85,7 +85,7 @@ const SelectedCategories: React.FC = ({ if (checkedItems.length === 0) return null return ( - + {checkedItems.map((item) => { const [category, subcategory] = item.split('_') return ( @@ -100,11 +100,11 @@ const SelectedCategories: React.FC = ({ } onDelete={() => handleDelete(item)} deleteIcon={} - className={cls(cmn.mri10, 'outlined', cmn.p600)} + className={cls('outlined', cmn.p600)} /> ) })} - + {filteredAppsCount} project{filteredAppsCount !== 1 ? 's' : ''} Date: Tue, 27 Aug 2024 18:29:26 +0100 Subject: [PATCH 3/5] Remove AppWithTimestamp, update Home page structure, fix filtering and sorting --- packages/core/src/types/ChainsMetadata.ts | 5 - packages/core/src/types/index.ts | 1 - src/Router.tsx | 33 +++-- src/components/HomeComponents.tsx | 75 ++++++++++ src/components/ecosystem/AllApps.tsx | 15 +- src/components/ecosystem/AppCardV2.tsx | 2 +- src/components/ecosystem/FavoriteApps.tsx | 42 +++--- src/components/ecosystem/NewApps.tsx | 32 ++-- src/components/ecosystem/TrendingApps.tsx | 111 +++++++------- src/core/ecosystem/utils.ts | 15 +- src/pages/Ecosystem.tsx | 94 +++++++----- src/pages/Home.tsx | 122 ++++++++++++++++ src/pages/Start.tsx | 169 ---------------------- src/useApps.tsx | 66 +++++++++ 14 files changed, 455 insertions(+), 327 deletions(-) create mode 100644 src/components/HomeComponents.tsx create mode 100644 src/pages/Home.tsx delete mode 100644 src/pages/Start.tsx create mode 100644 src/useApps.tsx diff --git a/packages/core/src/types/ChainsMetadata.ts b/packages/core/src/types/ChainsMetadata.ts index 8c0d7d7..567c136 100644 --- a/packages/core/src/types/ChainsMetadata.ts +++ b/packages/core/src/types/ChainsMetadata.ts @@ -59,11 +59,6 @@ export interface AppSocials { swell?: string; dappradar?: string; } -export interface AppWithTimestamp { - chain: string - app: string - added: number -} export interface CategoriesMap { [category: string]: string[] | null diff --git a/packages/core/src/types/index.ts b/packages/core/src/types/index.ts index 6518e5c..791bf40 100644 --- a/packages/core/src/types/index.ts +++ b/packages/core/src/types/index.ts @@ -27,7 +27,6 @@ export { AppMetadataMap, AppSocials, NetworksMetadataMap, - AppWithTimestamp, CategoriesMap, AppWithChainAndName } from './ChainsMetadata' diff --git a/src/Router.tsx b/src/Router.tsx index e3a3be9..421232e 100644 --- a/src/Router.tsx +++ b/src/Router.tsx @@ -1,3 +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 Router.tsx + * @copyright SKALE Labs 2024-Present + */ + import './App.scss' import { useState, useEffect } from 'react' @@ -35,7 +57,7 @@ import App from './pages/App' import History from './pages/History' import Portfolio from './pages/Portfolio' import Admin from './pages/Admin' -import Start from './pages/Start' +import Start from './pages/Home' import Staking from './pages/Staking' import StakeValidator from './pages/StakeValidator' import StakeAmount from './pages/StakeAmount' @@ -233,14 +255,7 @@ export default function Router() { - } + element={} /> } /> diff --git a/src/components/HomeComponents.tsx b/src/components/HomeComponents.tsx new file mode 100644 index 0000000..388d12e --- /dev/null +++ b/src/components/HomeComponents.tsx @@ -0,0 +1,75 @@ +/** + * @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 HomeComponents.tsx + * @copyright SKALE Labs 2024-Present + */ + +import SwapHorizontalCircleOutlinedIcon from '@mui/icons-material/SwapHorizontalCircleOutlined' +import PublicOutlinedIcon from '@mui/icons-material/PublicOutlined' +import FavoriteRoundedIcon from '@mui/icons-material/FavoriteRounded' +import LabelImportantRoundedIcon from '@mui/icons-material/LabelImportantRounded' +import RocketLaunchRoundedIcon from '@mui/icons-material/RocketLaunchRounded' +import TrendingUpRoundedIcon from '@mui/icons-material/TrendingUpRounded' +import LinkRoundedIcon from '@mui/icons-material/LinkRounded' +import PieChartOutlineRoundedIcon from '@mui/icons-material/PieChartOutlineRounded' +import OutboundRoundedIcon from '@mui/icons-material/OutboundRounded' + +interface SectionIcons { + [key: string]: JSX.Element +} + +export const SECTION_ICONS: SectionIcons = { + explore: , + favorites: , + new: , + trending: , + categories: +} + +interface ExploreCard { + name: string + description: string + icon: JSX.Element + url?: string +} + +export const EXPLORE_CARDS: ExploreCard[] = [ + { + name: 'bridge', + description: 'Transfer tokens between 50+ chains', + icon: + }, + { + name: 'stake', + description: 'Manage delegations and validators', + url: '/staking', + icon: + }, + { + name: 'SKALE Chains', + description: 'Chains info, block explorers and endpoints', + url: '/chains', + icon: + }, + { + name: 'ecosystem', + description: 'Discover apps and games on SKALE', + icon: + } +] diff --git a/src/components/ecosystem/AllApps.tsx b/src/components/ecosystem/AllApps.tsx index a0d9a63..a1892cc 100644 --- a/src/components/ecosystem/AllApps.tsx +++ b/src/components/ecosystem/AllApps.tsx @@ -15,14 +15,13 @@ * 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 { cls, cmn } from '@skalenetwork/metaport' +import { cls, cmn, SkPaper } from '@skalenetwork/metaport' import { type types } from '@/core' import { useLikedApps } from '../../LikedAppsContext' @@ -35,7 +34,7 @@ interface AllAppsProps { skaleNetwork: types.SkaleNetwork chainsMeta: types.ChainsMetadataMap apps: types.AppWithChainAndName[] - newApps: types.AppWithTimestamp[] + newApps: types.AppWithChainAndName[] loaded: boolean } @@ -46,7 +45,15 @@ const AllApps: React.FC = ({ skaleNetwork, chainsMeta, apps, newAp if (!loaded) return if (apps.length === 0) - return

No apps found

+ return ( + +
+

+ No apps match your current filters +

+
+
+ ) return ( diff --git a/src/components/ecosystem/AppCardV2.tsx b/src/components/ecosystem/AppCardV2.tsx index 726c4f7..802682c 100644 --- a/src/components/ecosystem/AppCardV2.tsx +++ b/src/components/ecosystem/AppCardV2.tsx @@ -41,7 +41,7 @@ export default function AppCard(props: { appName: string chainsMeta: types.ChainsMetadataMap transactions?: number - newApps?: types.AppWithTimestamp[] + newApps?: types.AppWithChainAndName[] isTrending?: boolean isNew?: boolean }) { diff --git a/src/components/ecosystem/FavoriteApps.tsx b/src/components/ecosystem/FavoriteApps.tsx index c6a1a98..4d554b0 100644 --- a/src/components/ecosystem/FavoriteApps.tsx +++ b/src/components/ecosystem/FavoriteApps.tsx @@ -20,16 +20,13 @@ * @file FavoriteApps.tsx * @copyright SKALE Labs 2024-Present */ - -import { useEffect, useMemo } from 'react' +import { 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' @@ -39,28 +36,23 @@ export default function FavoriteApps(props: { skaleNetwork: types.SkaleNetwork chainsMeta: types.ChainsMetadataMap useCarousel?: boolean - newApps: types.AppWithTimestamp[] + newApps: types.AppWithChainAndName[] + filteredApps: types.AppWithChainAndName[] + isSignedIn: boolean + error: string | null }) { - const { likedApps, error, refreshLikedApps, getAppInfoById, getTrendingApps } = useLikedApps() - const { isSignedIn } = useAuth() + const { getTrendingApps } = useLikedApps() const trendingAppIds = useMemo(() => getTrendingApps(), [getTrendingApps]) - useEffect(() => { - if (isSignedIn) { - refreshLikedApps() - } - }, [isSignedIn, refreshLikedApps]) - - if (!isSignedIn) return - if (error) return
Error: {error}
+ if (!props.isSignedIn) return + if (props.error) return
Error: {props.error}
- const appCards = likedApps.map((appId) => { - const { chain, app } = getAppInfoById(appId) - const isTrending = trendingAppIds.includes(appId) - const isNew = isNewApp({ chain, app }, props.newApps) + const appCards = props.filteredApps.map((app) => { + const isTrending = trendingAppIds.includes(`${app.chain}-${app.appName}`) + const isNew = isNewApp({ chain: app.chain, app: app.appName }, props.newApps) return (

- You don't have any favorites yet + {props.filteredApps.length === 0 + ? "You don't have any favorites yet" + : 'No favorite apps match your current filters'}

{props.useCarousel && (
diff --git a/src/components/ecosystem/NewApps.tsx b/src/components/ecosystem/NewApps.tsx index e8b53f4..cb1a066 100644 --- a/src/components/ecosystem/NewApps.tsx +++ b/src/components/ecosystem/NewApps.tsx @@ -9,13 +9,12 @@ * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . + * along with this program. If not, see . */ - /** * @file NewApps.tsx * @copyright SKALE Labs 2024-Present @@ -23,14 +22,14 @@ import React, { useMemo } from 'react' import { Grid, Box } from '@mui/material' -import { cls } from '@skalenetwork/metaport' +import { cls, cmn, SkPaper } 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 }[] + newApps: types.AppWithChainAndName[] skaleNetwork: types.SkaleNetwork chainsMeta: types.ChainsMetadataMap useCarousel?: boolean @@ -44,15 +43,16 @@ const NewApps: React.FC = ({ }) => { const { getTrendingApps, getAppId } = useLikedApps() const trendingAppIds = useMemo(() => getTrendingApps(), [getTrendingApps]) - const renderAppCard = (app: { chain: string; app: string }) => { - const appId = getAppId(app.chain, app.app) + + const renderAppCard = (app: types.AppWithChainAndName) => { + const appId = getAppId(app.chain, app.appName) const isTrending = trendingAppIds.includes(appId) return ( @@ -63,10 +63,22 @@ const NewApps: React.FC = ({ return {newApps.map(renderAppCard)} } + if (newApps.length === 0) { + return ( + +
+

+ No new apps match your current filters +

+
+
+ ) + } + return ( {newApps.map((app) => ( - + {renderAppCard(app)} ))} diff --git a/src/components/ecosystem/TrendingApps.tsx b/src/components/ecosystem/TrendingApps.tsx index a4037a1..025b7bc 100644 --- a/src/components/ecosystem/TrendingApps.tsx +++ b/src/components/ecosystem/TrendingApps.tsx @@ -1,32 +1,8 @@ -/** - * @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 { Box, Grid, Typography } from '@mui/material' +import { cls, cmn, SkPaper } from '@skalenetwork/metaport' import Carousel from '../Carousel' import { isNewApp } from '../../core/ecosystem/utils' import { getAppMeta } from '../../core/ecosystem/apps' @@ -35,54 +11,75 @@ interface TrendingAppsProps { skaleNetwork: types.SkaleNetwork chainsMeta: types.ChainsMetadataMap useCarousel?: boolean - newApps: types.AppWithTimestamp[] + newApps: types.AppWithChainAndName[] + trendingApps: types.AppWithChainAndName[] } const TrendingApps: React.FC = ({ skaleNetwork, chainsMeta, useCarousel, - newApps + newApps, + trendingApps }) => { - const { getTrendingApps, getAppInfoById } = useLikedApps() - const trendingAppIds = getTrendingApps() + const renderAppCards = () => { + return trendingApps.map((app) => { + const isNew = isNewApp({ chain: app.chain, app: app.appName }, newApps) + if (!getAppMeta(chainsMeta, app.chain, app.appName)) return null + + return ( + + + + + + ) + }) + } - const appCards = trendingAppIds.map((appId) => { - const { chain, app } = getAppInfoById(appId) + if (trendingApps.length === 0) { return ( - - - + +
+

+ No trending apps match your current filters +

+
+
) - }) + } - if (useCarousel) return {appCards} - - return ( - - {trendingAppIds.map((appId) => { - const { chain, app } = getAppInfoById(appId) - const isNew = isNewApp({ chain, app }, newApps) - if (!getAppMeta(chainsMeta, chain, app)) return null - return ( - - + if (useCarousel) { + return ( + + {trendingApps.map((app) => { + const isNew = isNewApp({ chain: app.chain, app: app.appName }, newApps) + if (!getAppMeta(chainsMeta, app.chain, app.appName)) return null + return ( + - - ) - })} + ) + })} + + ) + } + + return ( + + {renderAppCards()} ) } diff --git a/src/core/ecosystem/utils.ts b/src/core/ecosystem/utils.ts index 5a16bea..0f2c1ad 100644 --- a/src/core/ecosystem/utils.ts +++ b/src/core/ecosystem/utils.ts @@ -63,29 +63,28 @@ export const filterCategories = (searchTerm: string) => { export const getRecentApps = ( chainsMeta: types.ChainsMetadataMap, count: number = 12 -): types.AppWithTimestamp[] => { - const appsWithTimestamp: types.AppWithTimestamp[] = [] +): types.AppWithChainAndName[] => { + const appsWithTimestamp: types.AppWithChainAndName[] = [] Object.entries(chainsMeta).forEach(([chainName, chainData]) => { if (chainData.apps) { Object.entries(chainData.apps).forEach(([appName, appData]) => { if (appData.added) { appsWithTimestamp.push({ + ...appData, chain: chainName, - app: appName, - added: appData.added + appName: appName }) } }) } }) - - return appsWithTimestamp.sort((a, b) => b.added - a.added).slice(0, count) + return appsWithTimestamp.sort((a, b) => b.added! - a.added!).slice(0, count) } export const isNewApp = ( app: { chain: string; app: string }, - newApps: types.AppWithTimestamp[] + newApps: types.AppWithChainAndName[] ): boolean => { - return newApps.some((newApp) => newApp.chain === app.chain && newApp.app === app.app) + return newApps.some((newApp) => newApp.chain === app.chain && newApp.appName === app.app) } diff --git a/src/pages/Ecosystem.tsx b/src/pages/Ecosystem.tsx index de7b3bf..94f6bec 100644 --- a/src/pages/Ecosystem.tsx +++ b/src/pages/Ecosystem.tsx @@ -9,54 +9,41 @@ * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . + * along with this program. If not, see . */ - /** * @file Ecosystem.tsx * @copyright SKALE Labs 2024-Present */ -import { useState, useEffect, useMemo } from 'react' +import React, { 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 { Container, Stack, Box, 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 { filterAppsByCategory, filterAppsBySearchTerm } from '../core/ecosystem/apps' +import { useUrlParams } from '../core/ecosystem/urlParamsUtil' +import { SKALE_SOCIAL_LINKS } from '../core/constants' +import { useApps } from '../useApps' +import CategoryDisplay from '../components/ecosystem/Categories' 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: { @@ -66,18 +53,14 @@ export default function Ecosystem(props: { }) { const { getCheckedItemsFromUrl, setCheckedItemsInUrl, getTabIndexFromUrl, setTabIndexInUrl } = useUrlParams() - const allApps = useMemo(() => sortAppsByAlias(getAllApps(props.chainsMeta)), [props.chainsMeta]) + const { allApps, newApps, trendingApps, favoriteApps, isSignedIn } = useApps(props.chainsMeta) + const [checkedItems, setCheckedItems] = useState([]) const [filteredApps, setFilteredApps] = useState([]) const [searchTerm, setSearchTerm] = useState('') const [activeTab, setActiveTab] = useState(0) const [loaded, setLoaded] = useState(false) - const newApps = useMemo( - () => getRecentApps(props.chainsMeta, MAX_APPS_DEFAULT), - [props.chainsMeta] - ) - useEffect(() => { const initialCheckedItems = getCheckedItemsFromUrl() setCheckedItems(initialCheckedItems) @@ -93,7 +76,7 @@ export default function Ecosystem(props: { ) setFilteredApps(filtered) setLoaded(true) - }, [allApps, checkedItems, searchTerm]) + }, [allApps, checkedItems, searchTerm, props.chainsMeta]) const handleSetCheckedItems = (newCheckedItems: string[]) => { setCheckedItems(newCheckedItems) @@ -105,13 +88,42 @@ export default function Ecosystem(props: { setTabIndexInUrl(newValue) } - const filteredNewApps = useMemo(() => { - return newApps.filter((app) => - filteredApps.some( - (filteredApp) => filteredApp.chain === app.chain && filteredApp.appName === app.app - ) - ) - }, [newApps, filteredApps]) + const getFilteredAppsByTab = useMemo(() => { + const filterMap = new Map([ + [0, filteredApps], // All Apps + [ + 1, + newApps.filter((app) => + filteredApps.some( + (filteredApp) => filteredApp.chain === app.chain && filteredApp.appName === app.appName + ) + ) + ], // New Apps + [ + 2, + isSignedIn + ? favoriteApps.filter((app) => + filteredApps.some( + (filteredApp) => + filteredApp.chain === app.chain && filteredApp.appName === app.appName + ) + ) + : [] + ], // Favorite Apps + [ + 3, + trendingApps.filter((app) => + filteredApps.some( + (filteredApp) => filteredApp.chain === app.chain && filteredApp.appName === app.appName + ) + ) + ] // Trending Apps + ]) + + return (tabIndex: number) => filterMap.get(tabIndex) || filteredApps + }, [filteredApps, newApps, favoriteApps, trendingApps, isSignedIn]) + + const currentFilteredApps = getFilteredAppsByTab(activeTab) return ( @@ -149,7 +161,7 @@ export default function Ecosystem(props: { @@ -210,6 +222,9 @@ export default function Ecosystem(props: { chainsMeta={props.chainsMeta} skaleNetwork={props.mpc.config.skaleNetwork} newApps={newApps} + filteredApps={currentFilteredApps} + isSignedIn={isSignedIn} + error={null} /> )} {activeTab === 3 && ( @@ -217,6 +232,7 @@ export default function Ecosystem(props: { chainsMeta={props.chainsMeta} skaleNetwork={props.mpc.config.skaleNetwork} newApps={newApps} + trendingApps={currentFilteredApps} /> )} diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx new file mode 100644 index 0000000..6be86a8 --- /dev/null +++ b/src/pages/Home.tsx @@ -0,0 +1,122 @@ +import { Link } from 'react-router-dom' +import { Container, Stack, Box, Grid, Button } from '@mui/material' +import { cmn, cls } from '@skalenetwork/metaport' +import { type types } from '@/core' + +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 { SECTION_ICONS, EXPLORE_CARDS } from '../components/HomeComponents' + +interface HomeProps { + skaleNetwork: types.SkaleNetwork + chainsMeta: types.ChainsMetadataMap +} + +export default function Home({ skaleNetwork, chainsMeta }: HomeProps): JSX.Element { + const { newApps, trendingApps, favoriteApps, isSignedIn } = useApps(chainsMeta) + + return ( + + +

Welcome to SKALE

+ + + + } + /> + + } + /> + + } + /> +
+ + +
+ ) +} + +function ExploreSection(): JSX.Element { + return ( + + + {EXPLORE_CARDS.map((card, index) => ( + + + + ))} + + + ) +} + +interface AppSectionProps { + title: string + icon: JSX.Element + linkTo: string + component: JSX.Element +} + +function AppSection({ title, icon, linkTo, component }: AppSectionProps): JSX.Element { + return ( + <> +
+ + + + +
+ {component} + + ) +} diff --git a/src/pages/Start.tsx b/src/pages/Start.tsx deleted file mode 100644 index 6eb22ba..0000000 --- a/src/pages/Start.tsx +++ /dev/null @@ -1,169 +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 Start.tsx - * @copyright SKALE Labs 2023-Present - */ - -import { Link } from 'react-router-dom' - -import { cmn, cls } from '@skalenetwork/metaport' -import { type types } from '@/core' - -import Container from '@mui/material/Container' -import Stack from '@mui/material/Stack' -import Box from '@mui/material/Box' -import Grid from '@mui/material/Grid' -import { Button } from '@mui/material' - -import SwapHorizontalCircleOutlinedIcon from '@mui/icons-material/SwapHorizontalCircleOutlined' -import PublicOutlinedIcon from '@mui/icons-material/PublicOutlined' - -import FavoriteRoundedIcon from '@mui/icons-material/FavoriteRounded' -import LabelImportantRoundedIcon from '@mui/icons-material/LabelImportantRounded' -import RocketLaunchRoundedIcon from '@mui/icons-material/RocketLaunchRounded' -import TrendingUpRoundedIcon from '@mui/icons-material/TrendingUpRounded' -import LinkRoundedIcon from '@mui/icons-material/LinkRounded' -import PieChartOutlineRoundedIcon from '@mui/icons-material/PieChartOutlineRounded' -import OutboundRoundedIcon from '@mui/icons-material/OutboundRounded' - -import PageCard from '../components/PageCard' - -import { useEffect, useMemo, useState } from 'react' -import CategoryCardsGrid from '../components/ecosystem/CategoryCardsGrid' -import { getRecentApps } from '../core/ecosystem/utils' -import { MAX_APPS_DEFAULT } from '../core/constants' -import NewApps from '../components/ecosystem/NewApps' -import Headline from '../components/Headline' -import FavoriteApps from '../components/ecosystem/FavoriteApps' -import TrendingApps from '../components/ecosystem/TrendingApps' - -export default function Start(props: { - isXs: boolean - skaleNetwork: types.SkaleNetwork - loadData: () => Promise - chainsMeta: types.ChainsMetadataMap -}) { - const [_, setIntervalId] = useState() - const newApps = useMemo( - () => getRecentApps(props.chainsMeta, MAX_APPS_DEFAULT), - [props.chainsMeta] - ) - - useEffect(() => { - props.loadData() - const intervalId = setInterval(props.loadData, 10000) - setIntervalId(intervalId) - }, []) - - return ( - - -

Welcome to SKALE

- } - className={cls(cmn.mbott10, cmn.mtop20)} - /> - - - - } - /> - - - } - /> - - - } - /> - - - } - /> - - - -
- } /> - - - -
- -
- } - /> - - - -
- -
- } - /> - - - -
- -
- } - className={cls(cmn.mbott10, cmn.mtop20, cmn.ptop20)} - /> - -
- ) -} diff --git a/src/useApps.tsx b/src/useApps.tsx new file mode 100644 index 0000000..96e4bb1 --- /dev/null +++ b/src/useApps.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 useApps.tsx + * @copyright SKALE Labs 2024-Present + */ + +import { useMemo } from 'react' +import { type types } from '@/core' +import { getRecentApps } from './core/ecosystem/utils' +import { MAX_APPS_DEFAULT } from './core/constants' +import { useLikedApps } from './LikedAppsContext' +import { useAuth } from './AuthContext' + +export function useApps(chainsMeta: types.ChainsMetadataMap) { + const { getTrendingApps, likedApps, getAppId } = useLikedApps() + const { isSignedIn } = useAuth() + + const allApps = useMemo(() => { + const apps = Object.entries(chainsMeta).flatMap(([chainName, chainData]) => + Object.entries(chainData.apps || {}).map(([appName, app]) => ({ + chain: chainName, + appName, + ...app + })) + ) + return apps.sort((a, b) => a.alias.localeCompare(b.alias)) + }, [chainsMeta]) + + const newApps = useMemo(() => { + const apps = getRecentApps(chainsMeta, MAX_APPS_DEFAULT) + return apps.sort((a, b) => (b.added || 0) - (a.added || 0)) + }, [chainsMeta]) + + const trendingAppIds = getTrendingApps() + + const trendingApps = useMemo(() => { + const appMap = new Map(allApps.map((app) => [getAppId(app.chain, app.appName), app])) + return trendingAppIds + .map((id) => appMap.get(id)) + .filter((app): app is types.AppWithChainAndName => app !== undefined) + }, [allApps, trendingAppIds, getAppId]) + + const favoriteApps = useMemo(() => { + if (!isSignedIn) return [] + const apps = allApps.filter((app) => likedApps.includes(getAppId(app.chain, app.appName))) + return apps.sort((a, b) => a.alias.localeCompare(b.alias)) + }, [allApps, likedApps, isSignedIn, getAppId]) + + return { allApps, newApps, trendingApps, favoriteApps, isSignedIn } +} From 69eb45388daabb15018b3b5f49203e2537c735d3 Mon Sep 17 00:00:00 2001 From: Dmytro Date: Tue, 27 Aug 2024 18:39:48 +0100 Subject: [PATCH 4/5] Remove unused imports --- src/components/ecosystem/SelectedCategories.tsx | 11 +++++------ src/components/ecosystem/TrendingApps.tsx | 2 +- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/components/ecosystem/SelectedCategories.tsx b/src/components/ecosystem/SelectedCategories.tsx index 0898531..0c9a22e 100644 --- a/src/components/ecosystem/SelectedCategories.tsx +++ b/src/components/ecosystem/SelectedCategories.tsx @@ -23,7 +23,7 @@ import React from 'react' import { cmn, cls, styles } from '@skalenetwork/metaport' -import { Chip, Typography, Box } from '@mui/material' +import { Chip, Box } from '@mui/material' import CloseIcon from '@mui/icons-material/Close' import { categories } from '../../core/ecosystem/categories' @@ -104,17 +104,16 @@ const SelectedCategories: React.FC = ({ /> ) })} - +

{filteredAppsCount} project{filteredAppsCount !== 1 ? 's' : ''} - - +

Clear all - +

) } diff --git a/src/components/ecosystem/TrendingApps.tsx b/src/components/ecosystem/TrendingApps.tsx index 025b7bc..c3aa48e 100644 --- a/src/components/ecosystem/TrendingApps.tsx +++ b/src/components/ecosystem/TrendingApps.tsx @@ -1,7 +1,7 @@ import React from 'react' import { type types } from '@/core' import AppCard from './AppCardV2' -import { Box, Grid, Typography } from '@mui/material' +import { Box, Grid } from '@mui/material' import { cls, cmn, SkPaper } from '@skalenetwork/metaport' import Carousel from '../Carousel' import { isNewApp } from '../../core/ecosystem/utils' From a53a592f8d004325a058ec0cc7c8958881f19875 Mon Sep 17 00:00:00 2001 From: Dmytro Date: Tue, 27 Aug 2024 18:53:13 +0100 Subject: [PATCH 5/5] Add close button to CategoryDisplay component --- src/components/ecosystem/Categories.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/components/ecosystem/Categories.tsx b/src/components/ecosystem/Categories.tsx index 9d6e59a..ad1ac38 100644 --- a/src/components/ecosystem/Categories.tsx +++ b/src/components/ecosystem/Categories.tsx @@ -169,7 +169,7 @@ const CategoryDisplay: React.FC = ({ className="skMenu outlined" PaperProps={{ style: { - maxHeight: '500pt', + maxHeight: 'calc(80vh - 100px)', width: buttonRef.current?.offsetWidth, borderRadius: '25px', margin: '10px 0' @@ -177,6 +177,11 @@ const CategoryDisplay: React.FC = ({ }} >
+ {isXs && ( + + )}