Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove url-based navigation in favor of client states #990

Merged
merged 2 commits into from
Sep 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 2 additions & 7 deletions apps/web/app/(base-org)/ecosystem/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { Button, ButtonVariants } from 'apps/web/src/components/Button/Button';
import EcosystemHeroLogos from 'apps/web/public/images/ecosystem-hero-logos-new.png';
import { Divider } from 'apps/web/src/components/Divider/Divider';
import type { Metadata } from 'next';

import ImageAdaptive from 'apps/web/src/components/ImageAdaptive';
import Content from 'apps/web/src/components/Ecosystem/Content';

Expand Down Expand Up @@ -44,16 +43,12 @@ async function EcosystemHero() {
);
}

export type EcosystemProps = {
searchParams: { tag?: string; search?: string; showCount: number };
};

export default async function Ecosystem(page: EcosystemProps) {
export default async function Ecosystem() {
return (
<main className="flex w-full flex-col items-center bg-black">
<EcosystemHero />
<Divider />
<Content searchParams={page.searchParams} />
<Content />
</main>
);
}
3 changes: 2 additions & 1 deletion apps/web/src/components/Ecosystem/Card.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
'use client';
import ImageWithLoading from 'apps/web/src/components/ImageWithLoading';

type Props = {
Expand All @@ -12,7 +13,7 @@ function getNiceDomainDisplayFromUrl(url: string) {
return url.replace('https://', '').replace('http://', '').replace('www.', '').split('/')[0];
}

export async function Card({ name, url, description, imageUrl, tags }: Props) {
export function Card({ name, url, description, imageUrl, tags }: Props) {
return (
<a
href={url}
Expand Down
26 changes: 13 additions & 13 deletions apps/web/src/components/Ecosystem/Content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
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';
import { useState } from 'react';
import { List } from 'apps/web/src/components/Ecosystem/List';

export type EcosystemApp = {
Expand Down Expand Up @@ -42,14 +42,10 @@ const decoratedEcosystemApps: EcosystemApp[] = orderedEcosystemAppsAsc().map((d)
searchName: d.name.toLowerCase(),
}));

export type EcosystemProps = {
searchParams: { tag?: string; search?: string; showCount: number };
};

export default function Content({ searchParams }: EcosystemProps) {
const selectedTag = searchParams.tag ?? tags[0];
const search = searchParams.search ?? '';
const showCount = searchParams.showCount ? Number(searchParams.showCount) : 16;
export default function Content() {
const [selectedTag, setSelectedTag] = useState(tags[0]);
const [search, setSearch] = useState('');
const [showCount, setShowCount] = useState(16);

const filteredEcosystemApps = decoratedEcosystemApps.filter((app) => {
const isTagged = selectedTag === 'all' || app.tags.includes(selectedTag);
Expand All @@ -62,20 +58,24 @@ export default function Content({ searchParams }: EcosystemProps) {
<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} />
<TagChip
tag={tag}
isSelected={selectedTag === tag}
key={tag}
setSelectedTag={setSelectedTag}
/>
))}
</div>
<div className="order-first grow lg:order-last">
<Suspense>
<SearchBar value={search} />
</Suspense>
<SearchBar search={search} setSearch={setSearch} />
</div>
</div>
<List
selectedTag={selectedTag}
searchText={search}
apps={filteredEcosystemApps}
showCount={showCount}
setShowCount={setShowCount}
/>
</div>
);
Expand Down
25 changes: 14 additions & 11 deletions apps/web/src/components/Ecosystem/List.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,35 @@
'use client';
import ErrorImg from 'apps/web/public/images/error.png';
import { Button } from '../Button/Button';
import { Card } from './Card';
import { EcosystemApp } from 'apps/web/src/components/Ecosystem/Content';
import Link from 'next/link';
import { Url } from 'next/dist/shared/lib/router/router';
import ImageAdaptive from 'apps/web/src/components/ImageAdaptive';
import { Dispatch, SetStateAction, useCallback } from 'react';

export async function List({
export function List({
selectedTag,
searchText,
apps,
showCount,
setShowCount,
}: {
selectedTag: string;
searchText: string;
apps: EcosystemApp[];
showCount: number;
setShowCount: Dispatch<SetStateAction<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 },
};

const onClick = useCallback(
(event: React.MouseEvent<HTMLButtonElement>) => {
event.preventDefault();
setShowCount(showCount + 16);
},
[setShowCount, showCount],
);

return (
<>
Expand All @@ -45,9 +50,7 @@ export async function List({
)}
{canShowMore && (
<div className="mt-12 flex justify-center">
<Link href={tagHref} scroll={false}>
<Button>VIEW MORE</Button>
</Link>
<Button onClick={onClick}>VIEW MORE</Button>
</div>
)}
</>
Expand Down
48 changes: 15 additions & 33 deletions apps/web/src/components/Ecosystem/SearchBar.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
'use client';
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
import { useCallback, useRef, useState } from 'react';
import { Dispatch, SetStateAction, useCallback, useRef } from 'react';

function SearchIcon() {
return (
Expand Down Expand Up @@ -38,45 +37,28 @@ function XIcon() {
);
}

const DEBOUNCE_LENGTH_MS = 300;

export function SearchBar({ value }: { value: string }) {
const [text, setText] = useState(value);
export function SearchBar({
search,
setSearch,
}: {
search: string;
setSearch: Dispatch<SetStateAction<string>>;
}) {
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>) => {
clearTimeout(debounced.current);

const val = e.target.value;
setText(val);
updateRoute(val);

debounced.current = window.setTimeout(() => {}, DEBOUNCE_LENGTH_MS);
const value = e.target.value;
setSearch(value);
},
[updateRoute],
[setSearch],
);

const clearInput = useCallback(() => {
setText('');

updateRoute('');
}, [updateRoute]);
setSearch('');
}, [setSearch]);

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">
Expand All @@ -85,13 +67,13 @@ export function SearchBar({ value }: { value: string }) {
<input
type="text"
id="appsSearchBar"
value={text}
value={search}
onChange={onChange}
className="w-full bg-black font-sans text-base text-white placeholder:text-gray-muted focus:outline-none"
placeholder="Search"
aria-label="Search for apps and integrations in the Base ecosystem"
/>
{text && (
{search && (
<button type="button" onClick={clearInput} aria-label="clear input">
<XIcon />
</button>
Expand Down
39 changes: 22 additions & 17 deletions apps/web/src/components/Ecosystem/TagChip.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,31 @@
import { Url } from 'next/dist/shared/lib/router/router';
import Link from 'next/link';
'use client';

import { Dispatch, SetStateAction, useCallback } from 'react';

type Props = {
tag: string;
isSelected: boolean;
setSelectedTag: Dispatch<SetStateAction<string>>;
};

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 },
};
export function TagChip({ tag, isSelected, setSelectedTag }: Props) {
const onClick = useCallback(
(event: React.MouseEvent<HTMLButtonElement>) => {
event.preventDefault();
setSelectedTag(tag);
},
[setSelectedTag, tag],
);

return (
<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' : ''
}`}
>
<span className="text-center font-mono text-base uppercase text-white">{tag}</span>
</div>
</Link>
<button
onClick={onClick}
type="button"
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' : ''
}`}
>
<span className="text-center font-mono text-base uppercase text-white">{tag}</span>
</button>
);
}
Loading