mirror of
https://github.com/placeholder-soft/web.git
synced 2026-01-12 22:45:00 +08:00
[CHORE] Make ecosystem 100% SSR. (#759)
* Update ecosystem to full support SSR * use our fonts
This commit is contained in:
@@ -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 (
|
||||
<div className="mt-[-96px] flex w-full flex-col items-center bg-black pb-[96px]">
|
||||
@@ -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 (
|
||||
<main className="flex w-full flex-col items-center bg-black">
|
||||
<EcosystemHero />
|
||||
<Divider />
|
||||
<Suspense>
|
||||
<List />
|
||||
</Suspense>
|
||||
<div className="flex min-h-32 w-full max-w-[1440px] flex-col gap-10 px-8 pb-32">
|
||||
<div className="flex flex-col justify-between gap-8 lg:flex-row lg:gap-12">
|
||||
<div className="flex flex-row flex-wrap gap-3">
|
||||
{tags.map((tag) => (
|
||||
<TagChip tag={tag} isSelected={selectedTag === tag} key={tag} />
|
||||
))}
|
||||
</div>
|
||||
<div className="order-first grow lg:order-last">
|
||||
<Suspense>
|
||||
<SearchBar value={search} />
|
||||
</Suspense>
|
||||
</div>
|
||||
</div>
|
||||
<List
|
||||
selectedTag={selectedTag}
|
||||
searchText={search}
|
||||
apps={filteredEcosystemApps}
|
||||
showCount={showCount}
|
||||
/>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 (
|
||||
<Wallet>
|
||||
<ConnectWallet withWalletAggregator className="bg-transparent p-2 hover:bg-gray-40/20">
|
||||
<ConnectWallet withWalletAggregator className="bg-transparent p-2 hover:bg-gray-40/20">
|
||||
<UserAvatar />
|
||||
<Name chain={basenameChain} className={userAddressClasses} />
|
||||
</ConnectWallet>
|
||||
<WalletDropdown>
|
||||
<Identity className={classNames('px-4 pb-2 pt-3', className)} hasCopyAddressOnClick>
|
||||
<WalletDropdown className="font-sans">
|
||||
<Identity
|
||||
className={classNames('px-4 pb-2 pt-3 font-display', className)}
|
||||
hasCopyAddressOnClick
|
||||
>
|
||||
<UserAvatar />
|
||||
<Name chain={basenameChain} />
|
||||
<EthBalance />
|
||||
<Name chain={basenameChain} className="font-display" />
|
||||
<EthBalance className="font-display" />
|
||||
</Identity>
|
||||
<WalletDropdownBaseName />
|
||||
<WalletDropdownLink icon="wallet" href="https://wallet.coinbase.com">
|
||||
<WalletDropdownBaseName className="font-display" />
|
||||
<WalletDropdownLink
|
||||
icon="wallet"
|
||||
href="https://wallet.coinbase.com"
|
||||
target="_blank"
|
||||
className="font-display"
|
||||
>
|
||||
Go to Wallet Dashboard
|
||||
</WalletDropdownLink>
|
||||
<WalletDropdownDisconnect />
|
||||
<WalletDropdownDisconnect className="font-display" />
|
||||
</WalletDropdown>
|
||||
</Wallet>
|
||||
);
|
||||
|
||||
@@ -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 (
|
||||
<a
|
||||
href={url}
|
||||
@@ -23,7 +23,13 @@ export function Card({ name, url, description, imageUrl, tags }: Props) {
|
||||
>
|
||||
<div className="flex flex-row justify-between">
|
||||
<div className="relative h-[80px] w-[80px] overflow-hidden rounded-[3px]">
|
||||
<Image src={imageUrl} fill style={{ objectFit: 'contain' }} alt={`Logo of ${name}`} />
|
||||
<ImageWithLoading
|
||||
src={imageUrl}
|
||||
alt={`Logo of ${name}`}
|
||||
width={80}
|
||||
height={80}
|
||||
backgroundClassName="bg-black"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex h-6 flex-col justify-center rounded-[100px] bg-black px-2 py-1">
|
||||
<span className="font-mono text-xs uppercase text-white">{tags[0]}</span>
|
||||
|
||||
@@ -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 (
|
||||
<div className="flex min-h-32 w-full max-w-[1440px] flex-col gap-10 px-8 pb-32">
|
||||
<div className="flex flex-col justify-between gap-8 lg:flex-row lg:gap-12">
|
||||
<div className="flex flex-row flex-wrap gap-3">
|
||||
{TagList.map((tag) => (
|
||||
<TagChip
|
||||
tag={tag}
|
||||
isSelected={selectedTag === tag}
|
||||
setSelectedTag={selectTag}
|
||||
key={tag}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="order-first grow lg:order-last">
|
||||
<SearchBar setSearchText={setSearchText} />
|
||||
</div>
|
||||
</div>
|
||||
<>
|
||||
<div className="flex flex-col gap-10 lg:grid lg:grid-cols-4">
|
||||
{truncatedApps.map((app) => (
|
||||
<Card {...app} key={app.url} />
|
||||
@@ -112,12 +43,13 @@ export function List() {
|
||||
<span className="font-sans text-gray-muted">Try searching for another term</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{canShowMore && (
|
||||
<div className="mt-12 flex justify-center">
|
||||
<Button onClick={showMore}>VIEW MORE</Button>
|
||||
<Link href={tagHref} scroll={false}>
|
||||
<Button>VIEW MORE</Button>
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<svg
|
||||
@@ -42,9 +40,24 @@ function XIcon() {
|
||||
|
||||
const DEBOUNCE_LENGTH_MS = 300;
|
||||
|
||||
export function SearchBar({ setSearchText }: Props) {
|
||||
const [text, setText] = useState('');
|
||||
export function SearchBar({ value }: { value: string }) {
|
||||
const [text, setText] = useState(value);
|
||||
const debounced = useRef<number>();
|
||||
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<HTMLInputElement>) => {
|
||||
@@ -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 (
|
||||
<div className="flex h-10 flex-row items-center gap-2 rounded-[56px] border border-gray-60 p-2 md:w-full lg:w-80">
|
||||
|
||||
@@ -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<HTMLButtonElement>) => {
|
||||
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 (
|
||||
<button type="button" onClick={select}>
|
||||
<Link href={tagHref} scroll={false}>
|
||||
<div
|
||||
className={`flex h-10 shrink-0 cursor-pointer flex-col justify-center rounded-[100px] border border-gray-muted px-8 hover:border-white ${
|
||||
isSelected ? 'bg-gray-muted' : ''
|
||||
@@ -24,6 +21,6 @@ export function TagChip({ tag, isSelected, setSelectedTag }: Props) {
|
||||
>
|
||||
<span className="text-center font-mono text-base uppercase text-white">{tag}</span>
|
||||
</div>
|
||||
</button>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -55,6 +55,7 @@ export default function Banner({ href, text, bannerName }: BannerProps) {
|
||||
onClick={hideBanner}
|
||||
onKeyDown={hideBanner}
|
||||
type="button"
|
||||
aria-label="Close Banner"
|
||||
>
|
||||
<Icon name="close" color="black" width="16" height="16" />
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user