Skip to content

Commit

Permalink
Research Guide frontend: Do not use the removed identifier property a…
Browse files Browse the repository at this point in the history
…nd new top level layout.
  • Loading branch information
barbarah committed Dec 2, 2024
1 parent e583972 commit a33f818
Show file tree
Hide file tree
Showing 3 changed files with 220 additions and 43 deletions.
109 changes: 109 additions & 0 deletions apps/researcher/src/app/[locale]/research-guide/filterGuides.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import {filterLevel3Guides, sortLevel1Guides} from './filterGuides';

describe('filterLevel3Guides', () => {
it('only shows each level 3 guide once', () => {
const topLevel = {
id: 'top',
seeAlso: [
{
id: 'level1-1',
seeAlso: [
{
id: 'level2-1',
seeAlso: [{id: 'level3-1'}, {id: 'level3-2'}, {id: 'level3-3'}],
},
{
id: 'level2-2',
seeAlso: [{id: 'level3-1'}, {id: 'level3-4'}],
},
],
},
{
id: 'level1-2',
seeAlso: [
{
id: 'level2-3',
seeAlso: [{id: 'level3-2'}, {id: 'level3-5'}],
},
],
},
],
};

const filteredTopLevel = filterLevel3Guides(topLevel);

expect(filteredTopLevel.seeAlso?.[0].seeAlso?.[0].seeAlso).toEqual([
{id: 'level3-1'},
{id: 'level3-2'},
{id: 'level3-3'},
]);
expect(filteredTopLevel.seeAlso?.[0].seeAlso?.[1].seeAlso).toEqual([
{id: 'level3-4'},
]);
expect(filteredTopLevel.seeAlso?.[1].seeAlso?.[0].seeAlso).toEqual([
{id: 'level3-5'},
]);
});

it('filters out level 1 and 2 guides from the level 2 seeAlso', () => {
const topLevel = {
id: 'top',
seeAlso: [
{
id: 'level1-1',
seeAlso: [
{
id: 'level2-1',
seeAlso: [
{id: 'level1-2'}, // Level 1 guide
{id: 'level2-2'}, // Level 2 guide
{id: 'level3-1'}, // Level 3 guide
],
},
],
},
{
id: 'level1-2',
seeAlso: [
{
id: 'level2-2',
seeAlso: [
{id: 'level3-2'}, // Level 3 guide
],
},
],
},
],
};

const filteredTopLevel = filterLevel3Guides(topLevel);

expect(filteredTopLevel.seeAlso?.[0].seeAlso?.[0].seeAlso).toEqual([
{id: 'level3-1'},
]);
expect(filteredTopLevel.seeAlso?.[1].seeAlso?.[0].seeAlso).toEqual([
{id: 'level3-2'},
]);
});
});

describe('sortLevel1Guides', () => {
it('sorts level 1 guides by their names', () => {
const topLevel = {
id: 'top',
seeAlso: [
{id: '2', name: 'B'},
{id: '1', name: 'A'},
{id: '3', name: 'C'},
],
};

const sortedLevel1Guides = sortLevel1Guides(topLevel);

expect(sortedLevel1Guides).toEqual([
{id: '1', name: 'A'},
{id: '2', name: 'B'},
{id: '3', name: 'C'},
]);
});
});
60 changes: 60 additions & 0 deletions apps/researcher/src/app/[locale]/research-guide/filterGuides.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
type Guide = {
id: string;
name?: string;
seeAlso?: Guide[];
};

/**
* Filters out level 1 and level 2 guides from the seeAlso arrays of level 2 guides
* and ensures each level 3 guide is only shown once.
*
* Assumptions:
* - topLevel.seeAlso contains level 1 guides.
* - Each level 1 guide's seeAlso contains level 2 guides.
* - Each level 2 guide's seeAlso may contain level 1, level 2, and level 3 guides.
* - Level 3 guides should only be shown once across all level 2 guides.
*/
export const filterLevel3Guides = (topLevel: Guide): Guide => {
const displayedLevel3Guides = new Set<string>();

topLevel.seeAlso?.forEach(level1Guide => {
level1Guide.seeAlso?.forEach(level2Guide => {
const filteredSeeAlso =
level2Guide.seeAlso?.filter(guide => {
// Check if the guide is a level 1 guide
const isLevel1Guide = topLevel.seeAlso?.some(
l1 => l1.id === guide.id
);
// Check if the guide is a level 2 guide
const isLevel2Guide = topLevel.seeAlso?.some(
l1 => l1.seeAlso?.some(l2 => l2.id === guide.id)
);

// If the guide is not a level 1 or level 2 guide and has not been displayed yet, it's a level 3 guide
if (
!isLevel1Guide &&
!isLevel2Guide &&
!displayedLevel3Guides.has(guide.id)
) {
displayedLevel3Guides.add(guide.id);
return true;
}
return false;
}) || [];
level2Guide.seeAlso = filteredSeeAlso;
});
});

return topLevel;
};

/**
* Sorts the level 1 guides by their names.
*/
export const sortLevel1Guides = (topLevel: Guide): Guide[] => {
return (
topLevel.seeAlso?.sort((a, b) =>
(a.name || '').localeCompare(b.name || '')
) || []
);
};
94 changes: 51 additions & 43 deletions apps/researcher/src/app/[locale]/research-guide/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ import {Link} from '@/navigation';
import {ChevronRightIcon} from '@heroicons/react/24/solid';
import {getLocale, getTranslations} from 'next-intl/server';
import StringToMarkdown from './string-to-markdown';
import {
filterLevel3Guides,
sortLevel1Guides,
} from '@/app/[locale]/research-guide/filterGuides';

export default async function Page() {
const locale = (await getLocale()) as LocaleEnum;
Expand All @@ -17,11 +21,11 @@ export default async function Page() {

// There can be multiple top levels, but the current design only supports one.
const topLevel = topLevels[0];
const filteredTopLevel = filterLevel3Guides(topLevel);
const level1Guides = sortLevel1Guides(filteredTopLevel);

const level1Guides =
topLevel.seeAlso?.find(item => item.identifier === '1')?.seeAlso || [];
const level2Guides =
topLevel.seeAlso?.find(item => item.identifier === '2')?.seeAlso || [];
const firstLevel1Guide = level1Guides[0];
const nextLevel1Guides = level1Guides.slice(1);

return (
<>
Expand All @@ -31,58 +35,62 @@ export default async function Page() {
<div className="my-4 w-full max-w-5xl columns-2 gap-6">
{topLevel.text && <StringToMarkdown text={topLevel.text} />}
</div>
<div className="bg-consortium-sand-100 rounded mt-6 -mx-4 pr-10">
<h2 className="px-4 pt-4" tabIndex={0}>
{t('level1Title')}
</h2>
<div className="pb-4 columns-3 gap-10">
{level1Guides.map(item => (
<Link
key={item.id}
href={`/research-guide/${encodeRouteSegment(item.id)}`}
className="break-inside-avoid-column bg-consortium-sand-100 text-consortium-sand-800 no-underline hover:bg-consortium-sand-200 transition rounded flex flex-col py-2 px-4"
>
<div className="flex items-center justify-between gap-2">
<div>{item.name}</div>
<div>
<ChevronRightIcon className="w-5 h-5 fill--consortiumSand-900" />
{firstLevel1Guide && (
<div className="bg-consortium-sand-100 rounded mt-6 -mx-4 pr-10">
<h2 className="px-4 pt-4" tabIndex={0}>
{firstLevel1Guide.name}
</h2>
<div className="pb-4 columns-3 gap-10">
{firstLevel1Guide.seeAlso?.map(item => (
<Link
key={item.id}
href={`/research-guide/${encodeRouteSegment(item.id)}`}
className="break-inside-avoid-column bg-consortium-sand-100 text-consortium-sand-800 no-underline hover:bg-consortium-sand-200 transition rounded flex flex-col py-2 px-4"
>
<div className="flex items-center justify-between gap-2">
<div>{item.name}</div>
<div>
<ChevronRightIcon className="w-5 h-5 fill--consortiumSand-900" />
</div>
</div>
</div>
</Link>
))}
</Link>
))}
</div>
</div>
</div>
<div className="mt-10 flex flex-col lg:flex-row gap-10">
<div className="w-full lg:w-1/2">
)}
{nextLevel1Guides.map(level1Guide => (
<div className="mt-10" key={level1Guide.id}>
<h2 className="mb-4" tabIndex={0}>
{t('level2Title')}
{level1Guide.name}
</h2>
<div className="flex flex-col md:block md:columns-2 md:gap-6 *:break-inside">
{level2Guides.map(item => (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
{level1Guide.seeAlso?.map(level2Guides => (
<div
key={item.id}
className="bg-consortium-sand-100 text-consortium-sand-800 rounded flex flex-col p-2 mb-6"
key={level2Guides.id}
className="bg-consortium-sand-100 text-consortium-sand-800 rounded flex flex-col p-4"
>
<Link
href={`/research-guide/${encodeRouteSegment(item.id)}`}
href={`/research-guide/${encodeRouteSegment(
level2Guides.id
)}`}
className="flex items-center justify-between gap-2 no-underline"
>
<div className="flex items-center justify-between gap-2">
<div className="font-semibold">{item.name}</div>
<div>
<ChevronRightIcon className="w-5 h-5 fill--consortiumSand-900" />
</div>
<div className="font-semibold">{level2Guides.name}</div>
<div>
<ChevronRightIcon className="w-5 h-5 fill--consortiumSand-900" />
</div>
</Link>
{item.seeAlso?.map(subItem => (
{level2Guides.seeAlso?.map(linkedGuides => (
<Link
key={subItem.id}
href={`/research-guide/${encodeRouteSegment(subItem.id)}`}
className="no-underline hover:bg-consortium-sand-200 transition rounded flex flex-col p-2 -ml-2"
aria-label={`${subItem.name}, item of ${item.name}`}
key={linkedGuides.id}
href={`/research-guide/${encodeRouteSegment(
linkedGuides.id
)}`}
className="no-underline hover:bg-consortium-sand-200 transition rounded flex flex-col p-2 mt-2"
aria-label={`${linkedGuides.name}, item of ${level2Guides.name}`}
>
<div className="flex items-center justify-between gap-2">
<div>{subItem.name}</div>
<div>{linkedGuides.name}</div>
<div>
<ChevronRightIcon className="w-5 h-5 fill--consortiumSand-900" />
</div>
Expand All @@ -93,7 +101,7 @@ export default async function Page() {
))}
</div>
</div>
</div>
))}
</>
);
}

0 comments on commit a33f818

Please sign in to comment.