diff --git a/apps/web/app/(base-org)/ecosystem/page.tsx b/apps/web/app/(base-org)/ecosystem/page.tsx index fd7ae03..4543d50 100644 --- a/apps/web/app/(base-org)/ecosystem/page.tsx +++ b/apps/web/app/(base-org)/ecosystem/page.tsx @@ -4,8 +4,10 @@ import { Divider } from 'apps/web/src/components/Divider/Divider'; import { List } from 'apps/web/src/components/Ecosystem/List'; import type { Metadata } from 'next'; import Image from 'next/image'; +import ecosystemApps from 'apps/web/src/data/ecosystem.json'; +import { TagChip } from 'apps/web/src/components/Ecosystem/TagChip'; +import { SearchBar } from 'apps/web/src/components/Ecosystem/SearchBar'; import { Suspense } from 'react'; - export const metadata: Metadata = { metadataBase: new URL('https://base.org'), title: `Base | About`, @@ -15,6 +17,42 @@ export const metadata: Metadata = { }, }; +export type EcosystemApp = { + searchName: string; + name: string; + url: string; + description: string; + tags: string[]; + imageUrl: string; +}; + +const tags = [ + 'all', + ...ecosystemApps + .map((app) => app.tags) + .flat() + .filter((value, index, array) => { + return array.indexOf(value) === index; + }), +]; + +function orderedEcosystemAppsAsc() { + return ecosystemApps.sort((a, b) => { + if (a.name.toLowerCase() > b.name.toLowerCase()) { + return 1; + } + if (b.name.toLowerCase() > a.name.toLowerCase()) { + return -1; + } + return 0; + }); +} + +const decoratedEcosystemApps: EcosystemApp[] = orderedEcosystemAppsAsc().map((d) => ({ + ...d, + searchName: d.name.toLowerCase(), +})); + async function EcosystemHero() { return (
@@ -44,14 +82,45 @@ async function EcosystemHero() { ); } -export default async function Ecosystem() { +type EcosystemProps = { + searchParams: { tag?: string; search?: string; showCount: number }; +}; + +export default async function Ecosystem(page: EcosystemProps) { + const selectedTag = page.searchParams.tag ?? tags[0]; + const search = page.searchParams.search ?? ''; + const showCount = page.searchParams.showCount ? Number(page.searchParams.showCount) : 16; + + const filteredEcosystemApps = decoratedEcosystemApps.filter((app) => { + const isTagged = selectedTag === 'all' || app.tags.includes(selectedTag); + const isSearched = search === '' || app.name.toLowerCase().match(search.toLocaleLowerCase()); + return isTagged && isSearched; + }); + return (
- - - +
+
+
+ {tags.map((tag) => ( + + ))} +
+
+ + + +
+
+ +
); } diff --git a/apps/web/app/AppProviders.tsx b/apps/web/app/AppProviders.tsx index 8f3034d..9214bc0 100644 --- a/apps/web/app/AppProviders.tsx +++ b/apps/web/app/AppProviders.tsx @@ -1,5 +1,6 @@ 'use client'; - +import '@rainbow-me/rainbowkit/styles.css'; +import '@coinbase/onchainkit/styles.css'; import { Provider as CookieManagerProvider, Region, @@ -9,7 +10,6 @@ import { import { OnchainKitProvider } from '@coinbase/onchainkit'; import { Provider as TooltipProvider } from '@radix-ui/react-tooltip'; import { connectorsForWallets, RainbowKitProvider } from '@rainbow-me/rainbowkit'; -import '@rainbow-me/rainbowkit/styles.css'; import { coinbaseWallet, metaMaskWallet, diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx index 45cbb3c..d4ff43f 100644 --- a/apps/web/app/layout.tsx +++ b/apps/web/app/layout.tsx @@ -1,6 +1,7 @@ -import '@coinbase/onchainkit/styles.css'; import './global.css'; + import AppProviders from 'apps/web/app/AppProviders'; + import localFont from 'next/font/local'; import { Footer } from 'apps/web/src/components/Layout/Footer/Footer'; import DatadogInit from 'apps/web/app/datadog'; diff --git a/apps/web/src/components/ConnectWalletButton/ConnectWalletButton.tsx b/apps/web/src/components/ConnectWalletButton/ConnectWalletButton.tsx index 41a96d5..d65970a 100644 --- a/apps/web/src/components/ConnectWalletButton/ConnectWalletButton.tsx +++ b/apps/web/src/components/ConnectWalletButton/ConnectWalletButton.tsx @@ -84,7 +84,7 @@ export function ConnectWalletButton({ ); }, [openConnectModal]); - const userAddressClasses = classNames('text-lg', { + const userAddressClasses = classNames('text-lg font-display', { 'text-white': color === 'white', 'text-black': color === 'black', }); @@ -126,21 +126,29 @@ export function ConnectWalletButton({ return ( - + - - + + - - + + - - + + Go to Wallet Dashboard - + ); diff --git a/apps/web/src/components/Ecosystem/Card.tsx b/apps/web/src/components/Ecosystem/Card.tsx index 6bd80e6..d83c999 100644 --- a/apps/web/src/components/Ecosystem/Card.tsx +++ b/apps/web/src/components/Ecosystem/Card.tsx @@ -1,5 +1,5 @@ -/* eslint-disable react-perf/jsx-no-new-object-as-prop */ -import Image from 'next/image'; +'use client'; +import ImageWithLoading from 'apps/web/src/components/ImageWithLoading'; type Props = { name: string; @@ -13,7 +13,7 @@ function getNiceDomainDisplayFromUrl(url: string) { return url.replace('https://', '').replace('http://', '').replace('www.', '').split('/')[0]; } -export function Card({ name, url, description, imageUrl, tags }: Props) { +export async function Card({ name, url, description, imageUrl, tags }: Props) { return (
- {`Logo +
{tags[0]} diff --git a/apps/web/src/components/Ecosystem/List.tsx b/apps/web/src/components/Ecosystem/List.tsx index 06a789f..a90f63e 100644 --- a/apps/web/src/components/Ecosystem/List.tsx +++ b/apps/web/src/components/Ecosystem/List.tsx @@ -1,102 +1,33 @@ -'use client'; - -import { useCallback, useMemo, useState } from 'react'; import ErrorImg from 'apps/web/public/images/error.png'; -import data from 'apps/web/src/data/ecosystem.json'; import Image from 'next/image'; import { Button } from '../Button/Button'; import { Card } from './Card'; -import { SearchBar } from './SearchBar'; -import { TagChip } from './TagChip'; -import { usePathname, useRouter, useSearchParams } from 'next/navigation'; +import { EcosystemApp } from 'apps/web/app/(base-org)/ecosystem/page'; +import Link from 'next/link'; +import { Url } from 'next/dist/shared/lib/router/router'; -const TagList = [ - 'all', - 'bridge', - 'dao', - 'defi', - 'gaming', - // note in partnerCsvToEcosystemJson.js we remap infrastructure -> infra so it'll fit in the chip - 'infra', - 'nft', - 'onramp', - 'security', - 'social', - 'wallet', - 'x-chain', -]; - -function orderedDataAsc() { - return data.sort((a, b) => { - if (a.name.toLowerCase() > b.name.toLowerCase()) { - return 1; - } - if (b.name.toLowerCase() > a.name.toLowerCase()) { - return -1; - } - return 0; - }); -} - -const decoratedData = orderedDataAsc().map((d) => ({ - ...d, - searchName: d.name.toLowerCase(), -})); - -export function List() { - const router = useRouter(); - - const pathname = usePathname(); - const searchParams = useSearchParams(); - const selectedTag = searchParams?.get('tag') ?? 'all'; - - const [searchText, setSearchText] = useState(''); - const [showNum, setShowNum] = useState(16); - - const selectTag = useCallback( - (tag: string) => { - const params = new URLSearchParams(); - params.set('tag', tag); - router.push(pathname + '?' + params.toString(), { scroll: false }); - }, - [pathname, router], - ); - - const filteredApps = useMemo( - () => - decoratedData.filter((app) => { - const isTagged = selectedTag === 'all' || app.tags.includes(selectedTag); - - const isSearched = - searchText === '' || app.name.toLowerCase().match(searchText.toLocaleLowerCase()); - - return isTagged && isSearched; - }), - [selectedTag, searchText], - ); - const truncatedApps = useMemo(() => filteredApps.slice(0, showNum), [filteredApps, showNum]); - const canShowMore = showNum < filteredApps.length; - const showEmptyState = filteredApps.length === 0; - - const showMore = useCallback(() => setShowNum((num) => num + 16), [setShowNum]); +export async function List({ + selectedTag, + searchText, + apps, + showCount, +}: { + selectedTag: string; + searchText: string; + apps: EcosystemApp[]; + showCount: number; +}) { + const canShowMore = showCount < apps.length; + const showEmptyState = apps.length === 0; + const truncatedApps = apps.slice(0, showCount); + // eslint-disable-next-line react-perf/jsx-no-new-object-as-prop + const tagHref: Url = { + pathname: '/ecosystem', + query: { tag: selectedTag, search: searchText, showCount: showCount + 16 }, + }; return ( -
-
-
- {TagList.map((tag) => ( - - ))} -
-
- -
-
+ <>
{truncatedApps.map((app) => ( @@ -112,12 +43,13 @@ export function List() { Try searching for another term
)} - {canShowMore && (
- + + +
)} -
+ ); } diff --git a/apps/web/src/components/Ecosystem/SearchBar.tsx b/apps/web/src/components/Ecosystem/SearchBar.tsx index e4c237c..e810d24 100644 --- a/apps/web/src/components/Ecosystem/SearchBar.tsx +++ b/apps/web/src/components/Ecosystem/SearchBar.tsx @@ -1,9 +1,7 @@ +'use client'; +import { usePathname, useRouter, useSearchParams } from 'next/navigation'; import { useCallback, useRef, useState } from 'react'; -type Props = { - setSearchText: (val: string) => void; -}; - function SearchIcon() { return ( (); + const router = useRouter(); + const pathname = usePathname(); + const searchParams = useSearchParams(); + const tag = searchParams?.get('tag'); + + const updateRoute = useCallback( + (search: string) => { + const params = new URLSearchParams(searchParams?.toString()); + if (tag) params.set('tag', tag); + if (search) params.set('search', search); + if (!search) params.delete('search'); + router.push(pathname + '?' + params.toString(), { scroll: false }); + }, + [pathname, router, searchParams, tag], + ); const onChange = useCallback( (e: React.ChangeEvent) => { @@ -52,18 +65,18 @@ export function SearchBar({ setSearchText }: Props) { const val = e.target.value; setText(val); + updateRoute(val); - debounced.current = window.setTimeout(() => { - setSearchText(val); - }, DEBOUNCE_LENGTH_MS); + debounced.current = window.setTimeout(() => {}, DEBOUNCE_LENGTH_MS); }, - [setText, setSearchText], + [updateRoute], ); const clearInput = useCallback(() => { setText(''); - setSearchText(''); - }, [setSearchText]); + + updateRoute(''); + }, [updateRoute]); return (
diff --git a/apps/web/src/components/Ecosystem/TagChip.tsx b/apps/web/src/components/Ecosystem/TagChip.tsx index 97cce69..9be10d9 100644 --- a/apps/web/src/components/Ecosystem/TagChip.tsx +++ b/apps/web/src/components/Ecosystem/TagChip.tsx @@ -1,22 +1,19 @@ -import { useCallback } from 'react'; +import { Url } from 'next/dist/shared/lib/router/router'; +import Link from 'next/link'; type Props = { tag: string; isSelected: boolean; - setSelectedTag: (val: string) => void; }; -export function TagChip({ tag, isSelected, setSelectedTag }: Props) { - const select = useCallback( - (event: React.MouseEvent) => { - event.preventDefault(); - setSelectedTag(tag); - }, - [tag, setSelectedTag], - ); - +export async function TagChip({ tag, isSelected }: Props) { + // eslint-disable-next-line react-perf/jsx-no-new-object-as-prop + const tagHref: Url = { + pathname: '/ecosystem', + query: { tag }, + }; return ( - + ); } diff --git a/libs/base-ui/components/Layout/Nav/Banner.tsx b/libs/base-ui/components/Layout/Nav/Banner.tsx index d9b752c..ed2815c 100644 --- a/libs/base-ui/components/Layout/Nav/Banner.tsx +++ b/libs/base-ui/components/Layout/Nav/Banner.tsx @@ -55,6 +55,7 @@ export default function Banner({ href, text, bannerName }: BannerProps) { onClick={hideBanner} onKeyDown={hideBanner} type="button" + aria-label="Close Banner" >