Skip to content
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
58 changes: 54 additions & 4 deletions app/builders/page.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,21 @@
import { Suspense } from 'react';
import {
dehydrate,
HydrationBoundary,
QueryClient,
} from '@tanstack/react-query';

import {
BuildersView,
BuildersViewFallback,
} from '@/components/discover/builders-view';
import {
BUILDERS_REVALIDATE_SECONDS,
buildersKeys,
fetchBuilderFilters,
fetchBuilders,
parseBuildersQueryParams,
} from '@/components/discover/use-builders';
import { SiteFooter } from '@/components/layout/site-footer';
import { SiteHeader } from '@/components/layout/site-header';
import { JsonLd } from '@/components/seo/json-ld';
Expand Down Expand Up @@ -33,16 +45,54 @@ const buildersBreadcrumbs = breadcrumbSchema([
{ name: 'Builders', path: '/builders' },
]);

export default function BuildersPage() {
interface BuildersPageProps {
searchParams?: Promise<{ [key: string]: string | string[] | undefined }>;
}

export default async function BuildersPage(props: BuildersPageProps) {
const resolvedSearchParams = props.searchParams
? await props.searchParams
: {};
const queryParams = parseBuildersQueryParams(resolvedSearchParams);

const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 60_000,
},
},
});

await Promise.allSettled([
queryClient.prefetchQuery({
queryKey: buildersKeys.list(queryParams),
queryFn: () =>
fetchBuilders(queryParams, {
next: { revalidate: BUILDERS_REVALIDATE_SECONDS },
signal: AbortSignal.timeout(5000),
}),
}),
queryClient.prefetchQuery({
queryKey: buildersKeys.filters,
queryFn: () =>
fetchBuilderFilters({
next: { revalidate: BUILDERS_REVALIDATE_SECONDS },
signal: AbortSignal.timeout(5000),
}),
}),
]);

return (
<>
<JsonLd data={buildersSchema} />
<JsonLd data={buildersBreadcrumbs} />

<SiteHeader />
<Suspense fallback={<BuildersViewFallback />}>
<BuildersView />
</Suspense>
<HydrationBoundary state={dehydrate(queryClient)}>
<Suspense fallback={<BuildersViewFallback />}>
<BuildersView />
</Suspense>
</HydrationBoundary>
<SiteFooter />
</>
);
Expand Down
59 changes: 55 additions & 4 deletions app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,21 @@
import {
dehydrate,
HydrationBoundary,
QueryClient,
} from '@tanstack/react-query';
import Link from 'next/link';

import {
FeaturedProjects,
featuredProjectsQueryKey,
fetchFeaturedProjects,
fetchTopBuilders,
fetchTotal,
LANDING_REVALIDATE_SECONDS,
STATS,
StatsStrip,
TopBuilders,
topBuildersQueryKey,
WantToBuild,
} from '@/components/landing';
import { SiteFooter } from '@/components/layout/site-footer';
Expand All @@ -29,7 +41,44 @@ export const metadata: Metadata = {
},
};

export default function Home() {
export default async function Home() {
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 60_000,
},
},
});

await Promise.allSettled([
...STATS.map((stat) =>
queryClient.prefetchQuery({
queryKey: stat.queryKey,
queryFn: () =>
fetchTotal(stat.path, {
next: { revalidate: LANDING_REVALIDATE_SECONDS },
signal: AbortSignal.timeout(5000),
}),
})
),
queryClient.prefetchQuery({
queryKey: topBuildersQueryKey,
queryFn: () =>
fetchTopBuilders({
next: { revalidate: LANDING_REVALIDATE_SECONDS },
signal: AbortSignal.timeout(5000),
}),
}),
queryClient.prefetchQuery({
queryKey: featuredProjectsQueryKey,
queryFn: () =>
fetchFeaturedProjects({
next: { revalidate: LANDING_REVALIDATE_SECONDS },
signal: AbortSignal.timeout(5000),
}),
}),
]);

return (
<>
<SiteHeader />
Expand Down Expand Up @@ -60,9 +109,11 @@ export default function Home() {
</Section>
</HeroBackground>

<StatsStrip />
<TopBuilders />
<FeaturedProjects />
<HydrationBoundary state={dehydrate(queryClient)}>
<StatsStrip />
<TopBuilders />
<FeaturedProjects />
</HydrationBoundary>
<WantToBuild />

<SiteFooter />
Expand Down
68 changes: 17 additions & 51 deletions components/discover/builders-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,44 +21,20 @@ import {
} from "./filter-rail";
import { FilterSheet } from "./filter-sheet";
import {
BUILDERS_PAGE_SIZE,
BUILDERS_SEARCH_PARAM,
DEFAULT_BUILDER_SORT,
isBuilderSort,
parseBuildersQueryParams,
readBuilderFilters,
readBuilderPage,
readBuilderSort,
useBuilderFilters,
useBuilders,
type BuilderSort,
type BuildersQueryParams,
} from "./use-builders";

const PAGE_SIZE = 12;
const SEARCH_DEBOUNCE_MS = 300;
const SEARCH_PARAM = "q";

/**
* The query string is the source of truth for everything that narrows the
* list, so a filtered view can be linked, bookmarked, and walked back through
* with the browser buttons. Defaults are left out to keep the URL readable.
*/
function readFilters(params: URLSearchParams): FilterValue {
const country = params.get("country");
const status = params.get("status");
return {
// Repeated `skills` params, matching what the endpoint accepts. Joining on
// a comma would break any facet whose own label contains one.
skills: params.getAll("skills"),
country: country ? [country] : [],
status: status ? [status] : [],
};
}

function readPage(params: URLSearchParams): number {
const raw = Number(params.get("page"));
return Number.isInteger(raw) && raw > 0 ? raw : 1;
}

function readSort(params: URLSearchParams): BuilderSort {
const raw = params.get("sort");
return raw && isBuilderSort(raw) ? raw : DEFAULT_BUILDER_SORT;
}

function buildQuery(
search: string,
Expand All @@ -67,7 +43,7 @@ function buildQuery(
sort: BuilderSort,
): string {
const next = new URLSearchParams();
if (search) next.set(SEARCH_PARAM, search);
if (search) next.set(BUILDERS_SEARCH_PARAM, search);
for (const skill of filters.skills ?? []) next.append("skills", skill);
if (filters.country?.[0]) next.set("country", filters.country[0]);
if (filters.status?.[0]) next.set("status", filters.status[0]);
Expand Down Expand Up @@ -102,10 +78,10 @@ export function BuildersView() {
const router = useRouter();
const searchParams = useSearchParams();

const search = searchParams.get(SEARCH_PARAM) ?? "";
const filters = useMemo(() => readFilters(searchParams), [searchParams]);
const page = readPage(searchParams);
const sort = readSort(searchParams);
const search = searchParams.get(BUILDERS_SEARCH_PARAM) ?? "";
const filters = useMemo(() => readBuilderFilters(searchParams), [searchParams]);
const page = readBuilderPage(searchParams);
const sort = readBuilderSort(searchParams);

const [searchInput, setSearchInput] = useState(search);
const [railOpen, setRailOpen] = useState(true);
Expand Down Expand Up @@ -155,8 +131,8 @@ export function BuildersView() {
// reverting the sort. Everything except the term is carried through
// exactly as it stands at fire time.
const next = new URLSearchParams(window.location.search);
if (trimmed) next.set(SEARCH_PARAM, trimmed);
else next.delete(SEARCH_PARAM);
if (trimmed) next.set(BUILDERS_SEARCH_PARAM, trimmed);
else next.delete(BUILDERS_SEARCH_PARAM);
// Narrowing the list invalidates the page number.
next.delete("page");
const query = next.toString();
Expand All @@ -176,18 +152,8 @@ export function BuildersView() {
};

const params = useMemo<BuildersQueryParams>(
() => ({
page,
limit: PAGE_SIZE,
search: search || undefined,
// `GET /users/directory` takes a single value for these two, so their
// rail sections are single-select radios. Only `skills` is repeatable.
skills: filters.skills?.length ? filters.skills : undefined,
country: filters.country?.[0],
status: filters.status?.[0],
sort,
}),
[page, search, filters, sort],
() => parseBuildersQueryParams(searchParams),
[searchParams]
);

const { data, isError, isPending } = useBuilders(params);
Expand Down Expand Up @@ -294,7 +260,7 @@ export function BuildersView() {
isError={isError}
isNarrowed={hasActiveFilters(filters) || search.length > 0}
page={page}
pageSize={PAGE_SIZE}
pageSize={BUILDERS_PAGE_SIZE}
onPageChange={(next) => navigate(search, filters, next, "push")}
onHelpClick={() => setHelpDialogOpen(true)}
/>
Expand Down Expand Up @@ -336,7 +302,7 @@ export function BuildersViewFallback() {
subtitle="Meet the builders making an impact across the Boundless ecosystem."
/>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-4">
{Array.from({ length: PAGE_SIZE }, (_, index) => (
{Array.from({ length: BUILDERS_PAGE_SIZE }, (_, index) => (
<BuilderCardSkeleton key={index} showStats={false} />
))}
</div>
Expand Down
Loading