Skip to content

Commit

Permalink
Working project-list fetch (#144)
Browse files Browse the repository at this point in the history
* refactor: working project-list fetch

* fix: typos
  • Loading branch information
johnshift authored Apr 7, 2024
1 parent 944c930 commit a360f77
Show file tree
Hide file tree
Showing 11 changed files with 260 additions and 8 deletions.
38 changes: 38 additions & 0 deletions app/projects/@list/client-page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
'use client';

import dynamic from 'next/dynamic';

import { ROUTE_SECTIONS } from '~/shared/core/constants';

import { projectFiltersSearchParamsAtom } from '~/projects/atoms/project-filters-search-params-atoms';
import { projectTotalCountAtom } from '~/projects/atoms/project-total-count-atom';
import { FiltersProvider } from '~/filters/providers/filters-provider';

interface Props {
rawSearchParams: Record<string, string>;
}

export const ProjectListClientPage = ({ rawSearchParams }: Props) => {
return (
<FiltersProvider
rawSearchParams={rawSearchParams}
routeSection={ROUTE_SECTIONS.PROJECTS}
atom={projectFiltersSearchParamsAtom}
>
<FiltersSection
countAtom={projectTotalCountAtom}
searchPlaceholder="Search projects ..."
/>

<ProjectList />
</FiltersProvider>
);
};

const FiltersSection = dynamic(() =>
import('~/filters/components/filters-section').then((m) => m.FiltersSection),
);

const ProjectList = dynamic(() =>
import('~/projects/components/project-list').then((m) => m.ProjectList),
);
1 change: 1 addition & 0 deletions app/projects/@list/default.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './page';
41 changes: 41 additions & 0 deletions app/projects/@list/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { dehydrate, HydrationBoundary } from '@tanstack/react-query';

import { ROUTE_SECTIONS } from '~/shared/core/constants';
import { getQueryClient } from '~/shared/utils/get-query-client';

import { filterQueryKeys } from '~/filters/core/query-keys';
import { projectQueryKeys } from '~/projects/core/query-keys';
import { getFilterConfig } from '~/filters/api/get-filter-config';
import { getProjectList } from '~/projects/api/get-project-list';

import { ProjectListClientPage } from './client-page';

interface Props {
searchParams: Record<string, string>;
}

const ProjectListPage = async ({ searchParams: rawSearchParams }: Props) => {
const queryClient = getQueryClient();

await Promise.all([
// Prefetch list
queryClient.prefetchInfiniteQuery({
queryKey: projectQueryKeys.list(rawSearchParams),
queryFn: async ({ pageParam }) =>
getProjectList({ page: pageParam, searchParams: rawSearchParams }),
initialPageParam: 1,
}),
// Prefetch filter config
queryClient.prefetchQuery({
queryKey: filterQueryKeys.list(rawSearchParams, ROUTE_SECTIONS.PROJECTS),
queryFn: () => getFilterConfig(`/${ROUTE_SECTIONS.PROJECTS}`),
}),
]);

return (
<HydrationBoundary state={dehydrate(queryClient)}>
<ProjectListClientPage rawSearchParams={rawSearchParams} />
</HydrationBoundary>
);
};
export default ProjectListPage;
20 changes: 20 additions & 0 deletions app/projects/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
interface Props {
children: React.ReactNode;
list: React.ReactNode;
}

const ProjectsLayout = ({ children, list }: Props) => {
return (
<div className="min-h-screen w-full md:pl-[212px]">
<div className="w-full lg:w-1/2">
<div className="flex flex-col gap-8 px-2 py-8 pt-24 md:px-8 md:pt-8">
{list}
</div>
</div>

{children}
</div>
);
};

export default ProjectsLayout;
8 changes: 1 addition & 7 deletions app/projects/page.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,3 @@
const ProjectListPage = () => {
return (
<div className="flex min-h-screen w-full items-center justify-center">
ProjectListPage
</div>
);
};
const ProjectListPage = () => null;

export default ProjectListPage;
2 changes: 1 addition & 1 deletion src/projects/api/get-project-list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ interface Props {

export const getProjectList = ({ page, searchParams }: Props) => {
const url = createUrlWithSearchParams(
`${MW_URL}/organizations/list?page=${page}&limit=${PAGE_SIZE}`,
`${MW_URL}/projects/list?page=${page}&limit=${PAGE_SIZE}`,
searchParams,
);

Expand Down
5 changes: 5 additions & 0 deletions src/projects/atoms/init-project-atom.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { atom } from 'jotai';

import { ProjectDetails } from '~/projects/core/schemas';

export const initProjectAtom = atom<ProjectDetails | null>(null);
5 changes: 5 additions & 0 deletions src/projects/atoms/project-filters-search-params-atoms.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { atom } from 'jotai';

export const projectFiltersSearchParamsAtom = atom<URLSearchParams>(
new URLSearchParams(),
);
3 changes: 3 additions & 0 deletions src/projects/atoms/project-total-count-atom.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { atom } from 'jotai';

export const projectTotalCountAtom = atom<number | null>(null);
65 changes: 65 additions & 0 deletions src/projects/components/project-list.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
'use client';

import { cn } from '~/shared/utils/cn';
import { reloadPage } from '~/shared/utils/reload-page';
import { InternalErrorResult } from '~/shared/components/internal-error-result';
import { VirtualWrapper } from '~/shared/components/virtual-wrapper';

import { useProjectList } from '~/projects/hooks/use-project-list';
import { useFiltersContext } from '~/filters/providers/filters-provider/context';

export const ProjectList = () => {
const {
projects,
error,
inViewRef,
hasNextPage,
isSuccess,
isPending: isPendingList,
} = useProjectList();
const hasProjects = projects.length > 0;

const { isPendingFilters, filterSearchParams } = useFiltersContext();

const isPending = [isPendingFilters, isPendingList].includes(true);

const filterParamsString =
filterSearchParams.size > 0 ? `${filterSearchParams.toString()}` : '';

return (
<>
{error && <InternalErrorResult onReset={reloadPage} />}

{isPending ? (
<p>Card Skeleton</p>
) : isSuccess && hasProjects ? (
<>
<div>
<p>Init project card</p>
<p>{JSON.stringify({ filterParamsString })}</p>
</div>

<VirtualWrapper count={projects.length}>
{(index) => (
<div className={cn({ 'pt-8': index > 0 })}>
<div className="flex flex-col bg-darkest-gray p-4">
<pre>{JSON.stringify(projects[index], undefined, '\t')}</pre>
</div>
</div>
)}
</VirtualWrapper>

{hasNextPage ? (
<div ref={inViewRef}>
<p>Card Skeleton</p>
</div>
) : (
<p>TODO: No more projects UI</p>
)}
</>
) : (
<p>Empty UI</p>
)}
</>
);
};
80 changes: 80 additions & 0 deletions src/projects/hooks/use-project-list.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { useEffect } from 'react';
import { useInView } from 'react-intersection-observer';

import { useAtom } from 'jotai';

import { HREFS } from '~/shared/core/constants';
import { getQueryClient } from '~/shared/utils/get-query-client';
import { initPathAtom } from '~/shared/atoms/init-path-atom';

import { projectQueryKeys } from '~/projects/core/query-keys';
import { initProjectAtom } from '~/projects/atoms/init-project-atom';
import { projectTotalCountAtom } from '~/projects/atoms/project-total-count-atom';
import { getProjectDetails } from '~/projects/api/get-project-details';

import { useProjectListQuery } from './use-project-list-query';

export const useProjectList = () => {
const queryClient = getQueryClient();

const {
data,
error,
fetchNextPage,
hasNextPage,
isSuccess,
isPending,
isFetching,
} = useProjectListQuery();

// Sync total count
const [totalCount, setTotalCount] = useAtom(projectTotalCountAtom);
useEffect(() => {
const currentTotal = data?.pages[0].total ?? 0;
if (data && currentTotal !== totalCount) {
setTotalCount(currentTotal);
}
}, [data, setTotalCount, totalCount]);

// Prefetch project details
useEffect(() => {
if (isSuccess && data) {
const items = data.pages.flatMap((d) => d.data);
for (const item of items) {
const { id } = item;
queryClient.prefetchQuery({
queryKey: projectQueryKeys.details(id),
queryFn: () => getProjectDetails({ projectId: id }),
});
}
}
}, [data, isSuccess, queryClient]);

// Next page fetch on scroll
const { ref: inViewRef } = useInView({
threshold: 1,
onChange: (inView) => {
if (inView && !error && !isFetching) fetchNextPage();
},
});

const [initPath] = useAtom(initPathAtom);
const isProjectListSSR = initPath === HREFS.PROJECTS_PAGE;

const [initProject] = useAtom(initProjectAtom);
const allProjects = data?.pages.flatMap((d) => d.data) ?? [];

// Dedupe init-card if not list-page ssr
const projects = !isProjectListSSR
? allProjects.filter((d) => d.id !== initProject?.id)
: allProjects;

return {
projects,
error,
inViewRef,
hasNextPage,
isSuccess,
isPending,
};
};

0 comments on commit a360f77

Please sign in to comment.