Skip to content

Commit

Permalink
refactor: restore hidden-item implementation for pillar-info and drop…
Browse files Browse the repository at this point in the history
…down
  • Loading branch information
johnshift committed Dec 11, 2024
1 parent 3f4b7e2 commit a114c44
Show file tree
Hide file tree
Showing 8 changed files with 150 additions and 61 deletions.
41 changes: 40 additions & 1 deletion src/search/components/pillar-item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,64 @@

import Link from 'next/link';
import { useRouter } from 'next/navigation';
import { useInView } from 'react-intersection-observer';

import { Button } from '@nextui-org/button';
import { useSetAtom } from 'jotai';

import { cn } from '@/shared/utils/cn';

import { TPillarItem } from '@/search/core/types';
import { hiddenPillarItemsAtom } from '@/search/core/atoms';

import { usePillarRoutesContext } from '@/search/state/contexts/pillar-routes-context';

interface Props {
isMainPillarItem: boolean;
item: TPillarItem;
pillarSlug: string;
}

export const PillarItem = ({ item, isMainPillarItem }: Props) => {
export const PillarItem = ({ item, isMainPillarItem, pillarSlug }: Props) => {
const { isActive, label, href } = item;

const router = useRouter();
const { isPendingPillarRoute, startTransition } = usePillarRoutesContext();

const setHiddenItems = useSetAtom(hiddenPillarItemsAtom);
const { ref: inViewRef } = useInView({
onChange: (inView) => {
setHiddenItems((prev) => {
const newState = { ...prev };

// Initialize Set if pillar doesn't exist
if (!newState[pillarSlug]) {
newState[pillarSlug] = [];
}

// Update hidden items set
let pillarHiddenItems = [...newState[pillarSlug]];

if (!inView) {
pillarHiddenItems.unshift(label);
} else {
pillarHiddenItems = pillarHiddenItems.filter(
(itemLabel) => itemLabel !== label,
);
}

// Remove pillar key if empty
if (pillarHiddenItems.length > 0) {
newState[pillarSlug] = pillarHiddenItems;
} else {
delete newState[pillarSlug];
}

return newState;
});
},
});

const onClick = () => {
startTransition(() => {
router.push(href);
Expand All @@ -39,6 +77,7 @@ export const PillarItem = ({ item, isMainPillarItem }: Props) => {
<Button
as={Link}
href={href}
ref={inViewRef}
radius="md"
className={className}
variant={variant}
Expand Down
22 changes: 16 additions & 6 deletions src/search/components/pillar-items-dropdown-content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { Listbox, ListboxItem } from '@nextui-org/listbox';
import { ScrollShadow } from '@nextui-org/scroll-shadow';
import { Spinner } from '@nextui-org/spinner';
import { useQueryClient } from '@tanstack/react-query';
import { useAtomValue } from 'jotai';

import { cn } from '@/shared/utils/cn';
import { normalizeString } from '@/shared/utils/normalize-string';
Expand All @@ -25,6 +26,7 @@ import {
import { convertSlugToTitle } from '@/search/utils/convert-slug-to-title';
import { createPillarItemHref } from '@/search/utils/create-pillar-item-href';
import { createToggledPillarItemSearchParam } from '@/search/utils/create-toggled-pillar-item-search-param';
import { hiddenPillarItemsAtom } from '@/search/core/atoms';
import { getPillarItems } from '@/search/data/get-pillar-items';

import { usePillarRoutesContext } from '@/search/state/contexts/pillar-routes-context';
Expand Down Expand Up @@ -56,6 +58,9 @@ export const PillarItemsDropdownContent = (props: Props) => {
setQuery(event.target.value);
};

const hiddenItemsMap = useAtomValue(hiddenPillarItemsAtom);
const hiddenItems = hiddenItemsMap[pillarSlug] || [];

const list = useAsyncList<string, number>({
async load({ cursor, filterText }) {
const queryProps: GetPillarItemsProps = {
Expand All @@ -66,7 +71,7 @@ export const PillarItemsDropdownContent = (props: Props) => {
limit: ITEMS_PER_PAGE,
};

const items = await queryClient.fetchQuery({
const responseItems = await queryClient.fetchQuery({
queryKey: searchQueryKeys.getPillarItems(queryProps),
queryFn: async () => getPillarItems(queryProps),
});
Expand All @@ -75,21 +80,26 @@ export const PillarItemsDropdownContent = (props: Props) => {
const nextCursor = start + ITEMS_PER_PAGE;

return {
items,
items: cursor ? responseItems : [...hiddenItems, ...responseItems],
cursor: nextCursor,
};
},
});

const itemLabelsSet = useMemo(
() => new Set(activeItems.map(({ label }) => label)),
useEffect(() => {
list.reload();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [hiddenItems]);

const itemSlugsSet = useMemo(
() => new Set(activeItems.map(({ label }) => normalizeString(label))),
[activeItems],
);

const onAction = (key: React.Key) => {
if (key) {
const itemSlug = normalizeString(key as string);
const isActive = itemLabelsSet.has(key as string);
const isActive = itemSlugsSet.has(itemSlug);
const newSearchParams = createToggledPillarItemSearchParam({
itemSlug,
pillarParamKey,
Expand Down Expand Up @@ -149,7 +159,7 @@ export const PillarItemsDropdownContent = (props: Props) => {
>
{list.items.length > 0 ? (
list.items.map((label, i) => {
const isActive = itemLabelsSet.has(label);
const isActive = itemSlugsSet.has(normalizeString(label));

return (
<ListboxItem
Expand Down
15 changes: 11 additions & 4 deletions src/search/components/pillar-items.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,31 @@
import { normalizeString } from '@/shared/utils/normalize-string';

import { TPillarItem } from '@/search/core/types';
import { PillarParams, TPillarItem } from '@/search/core/types';
import { PillarItem } from '@/search/components/pillar-item';
import { PillarItemsDropdown } from '@/search/components/pillar-items-dropdown';

interface Props {
items: TPillarItem[];
dropdownContent: React.ReactNode;
itemParam: string;
params: PillarParams;
pillarSlug: string;
}

export const PillarItems = ({ items, dropdownContent, itemParam }: Props) => {
export const PillarItems = ({
items,
dropdownContent,
params,
pillarSlug,
}: Props) => {
return (
<div className="relative flex h-14 gap-4 overflow-hidden p-1">
<div className="flex max-w-fit flex-wrap gap-4">
{items.map((item) => (
<PillarItem
key={item.label}
isMainPillarItem={normalizeString(item.label) === itemParam}
isMainPillarItem={normalizeString(item.label) === params.item}
item={item}
pillarSlug={pillarSlug}
/>
))}
</div>
Expand Down
7 changes: 3 additions & 4 deletions src/search/core/atoms.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { atom } from 'jotai';

import { TPillarItemMap } from '@/search/core/types';

export const searchQueryAtom = atom<string>('');
export const hiddenMainPillarItemsAtom = atom<TPillarItemMap>(new Map());
export const hiddenAltPillarItemsAtom = atom<TPillarItemMap>(new Map());

type TPillarItemMap = Record<string, string[]>;
export const hiddenPillarItemsAtom = atom<TPillarItemMap>({});
2 changes: 0 additions & 2 deletions src/search/core/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,3 @@ export interface TPillarItem {
href: string;
isActive: boolean;
}

export type TPillarItemMap = Map<string, TPillarItem>;
79 changes: 37 additions & 42 deletions src/search/data/get-pillar-info.ts
Original file line number Diff line number Diff line change
@@ -1,51 +1,46 @@
// import { MW_URL } from '@/shared/core/envs';
// import { mwGET } from '@/shared/utils/mw-get';

// import {
// dtoToPillarInfo,
// pillarInfoResponseSchema,
// TPillarInfo,
// } from '@/search/core/schemas';
// import { GetPillarInfoProps } from '@/search/core/types';

// export const getPillarInfo = async (
// props: GetPillarInfoProps,
// ): Promise<TPillarInfo> => {
// const { nav, pillar, item, pillar2, item2, limit } = props;
import { MW_URL } from '@/shared/core/envs';
import { mwGET } from '@/shared/utils/mw-get';

import {
dtoToPillarInfo,
pillarInfoResponseSchema,
TPillarInfo,
} from '@/search/core/schemas';
import { GetPillarInfoProps } from '@/search/core/types';

// const url = new URL(`${MW_URL}/search/pillar`);
// url.searchParams.set('nav', nav);
// url.searchParams.set('pillar', pillar);
// url.searchParams.set('item', item);
export const getPillarInfo = async (
props: GetPillarInfoProps,
): Promise<TPillarInfo> => {
const { nav, pillar, item, limit } = props;

// if (typeof pillar2 === 'string' && typeof item2 === 'string') {
// url.searchParams.set('pillar2', pillar2);
// url.searchParams.set('item2', item2);
// }
const url = new URL(`${MW_URL}/search/pillar`);
url.searchParams.set('nav', nav);
url.searchParams.set('pillar', pillar);
url.searchParams.set('item', item);

// if (limit) url.searchParams.set('limit', limit.toString());
if (limit) url.searchParams.set('limit', limit.toString());

// const response = await mwGET({
// url: url.toString(),
// label: 'getPillarInfo',
// responseSchema: pillarInfoResponseSchema,
// });
const response = await mwGET({
url: url.toString(),
label: 'getPillarInfo',
responseSchema: pillarInfoResponseSchema,
});

// if (!response.success) {
// throw new Error(response.message);
// }
if (!response.success) {
throw new Error(response.message);
}

// return dtoToPillarInfo(response.data);
// };
return dtoToPillarInfo(response.data);
};

import { TPillarInfo } from '@/search/core/schemas';
import { GetPillarInfoProps } from '@/search/core/types';
// import { TPillarInfo } from '@/search/core/schemas';
// import { GetPillarInfoProps } from '@/search/core/types';

import { fakePillarInfo } from '@/search/testutils/fake-pillar-info';
// import { fakePillarInfo } from '@/search/testutils/fake-pillar-info';

export const getPillarInfo = async (
props: GetPillarInfoProps,
): Promise<TPillarInfo> => {
await new Promise((r) => setTimeout(r, 500));
return fakePillarInfo(props.limit);
};
// export const getPillarInfo = async (
// props: GetPillarInfoProps,
// ): Promise<TPillarInfo> => {
// await new Promise((r) => setTimeout(r, 500));
// return fakePillarInfo(props.limit);
// };
6 changes: 4 additions & 2 deletions src/search/pages/pillar-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,9 @@ export const PillarPage = async ({ nav, params, searchParams }: Props) => {
description={description}
items={
<PillarItems
itemParam={params.item}
params={params}
items={mainItems}
pillarSlug={params.pillar}
dropdownContent={
<PillarItemsDropdownContent
nav={nav}
Expand All @@ -81,8 +82,9 @@ export const PillarPage = async ({ nav, params, searchParams }: Props) => {
return (
<div key={slug} className="px-4">
<PillarItems
itemParam={params.item}
params={params}
items={items}
pillarSlug={slug}
dropdownContent={
<PillarItemsDropdownContent
nav={nav}
Expand Down
39 changes: 39 additions & 0 deletions src/search/state/contexts/pillar-items-context.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { createContext, useContext } from 'react';

import { TPillarItem } from '@/search/core/types';

interface PillarItemsCtx {
items: TPillarItem[];
}

export const PillarItemsContext = createContext<PillarItemsCtx | undefined>(
undefined,
);

export const usePillarItemsContext = () => {
const context = useContext(PillarItemsContext);

if (!context) {
throw new Error(
'usePillarItemsContext must be used within a PillarItemsContext',
);
}

return context;
};

// NOTE: items can be a lot, and we dont want to serialize all items for every item
// This provider lets us not serialize all items for every item
export const PillarItemsProvider = ({
items,
children,
}: {
items: TPillarItem[];
children: React.ReactNode;
}) => {
return (
<PillarItemsContext.Provider value={{ items }}>
{children}
</PillarItemsContext.Provider>
);
};

0 comments on commit a114c44

Please sign in to comment.