-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
NickAkhmetov/HMP-500 - Biomarkers landing page (#3330)
Co-authored-by: John Conroy <[email protected]>
- Loading branch information
1 parent
11b9723
commit c6c116f
Showing
30 changed files
with
632 additions
and
124 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
- Added biomarkers landing page. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
159 changes: 159 additions & 0 deletions
159
context/app/static/js/components/biomarkers/BiomarkersPanelItem.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,159 @@ | ||
import React from 'react'; | ||
|
||
import Typography from '@mui/material/Typography'; | ||
import Box from '@mui/material/Box'; | ||
import Stack, { StackProps } from '@mui/material/Stack'; | ||
import Chip from '@mui/material/Chip'; | ||
import { styled } from '@mui/material/styles'; | ||
|
||
import LineClamp from 'js/shared-styles/text/LineClamp'; | ||
import { InternalLink } from 'js/shared-styles/Links'; | ||
import { useIsMobile } from 'js/hooks/media-queries'; | ||
import SelectableChip from 'js/shared-styles/chips/SelectableChip'; | ||
import { SecondaryBackgroundTooltip } from 'js/shared-styles/tooltips'; | ||
import { useBiomarkersSearchActions, useBiomarkersSearchState } from './BiomarkersSearchContext'; | ||
|
||
const mobileStackProps: Partial<StackProps> = { | ||
height: 'unset', | ||
direction: 'column', | ||
spacing: 2, | ||
py: 2, | ||
}; | ||
|
||
const desktopStackProps: Partial<StackProps> = { | ||
height: 52, | ||
direction: 'row', | ||
spacing: 4, | ||
py: 0, | ||
}; | ||
|
||
function StackTemplate(props: React.ComponentProps<typeof Stack>) { | ||
const isMobile = useIsMobile(); | ||
const responsiveProps = isMobile ? mobileStackProps : desktopStackProps; | ||
return <Stack px={2} useFlexGap width="100%" {...responsiveProps} {...props} />; | ||
} | ||
|
||
function MobileLabel({ children }: { children: React.ReactNode }) { | ||
const isMobile = useIsMobile(); | ||
if (!isMobile) { | ||
return null; | ||
} | ||
return ( | ||
<Typography component="label" width="33%" flexShrink={0} pr={2}> | ||
{children} | ||
</Typography> | ||
); | ||
} | ||
|
||
function BodyCell({ children, ...props }: React.ComponentProps<typeof Box>) { | ||
const ariaLabel = props['aria-label']; | ||
return ( | ||
<Box display="flex" alignItems="center" {...props}> | ||
<MobileLabel>{ariaLabel}</MobileLabel> | ||
{children} | ||
</Box> | ||
); | ||
} | ||
|
||
function HeaderCell({ children, ...props }: React.ComponentProps<typeof Box>) { | ||
return ( | ||
<BodyCell {...props}> | ||
<Typography variant="subtitle2">{children}</Typography> | ||
</BodyCell> | ||
); | ||
} | ||
|
||
const desktopConfig = { | ||
name: { | ||
flexBasis: '30%', | ||
flexGrow: 1, | ||
}, | ||
description: { | ||
flexBasis: '40%', | ||
flexGrow: 1, | ||
}, | ||
type: { | ||
flexBasis: 'fit-content', | ||
flexShrink: 0, | ||
flexGrow: 1, | ||
}, | ||
}; | ||
|
||
function BiomarkerHeaderPanel() { | ||
const isMobile = useIsMobile(); | ||
if (isMobile) { | ||
return null; | ||
} | ||
return ( | ||
<StackTemplate spacing={1}> | ||
<HeaderCell {...desktopConfig.name}>Name</HeaderCell> | ||
<HeaderCell {...desktopConfig.description}>Description</HeaderCell> | ||
<HeaderCell {...desktopConfig.type}>Type</HeaderCell> | ||
</StackTemplate> | ||
); | ||
} | ||
|
||
interface BiomarkerPanelItemProps { | ||
name: string; | ||
href?: string; | ||
description: string; | ||
type: string; | ||
} | ||
|
||
const UnroundedChip = styled(Chip)(({ theme }) => ({ | ||
borderRadius: theme.spacing(1), | ||
'&.MuiChip-outlined': { | ||
borderRadius: theme.spacing(1), | ||
}, | ||
})); | ||
|
||
function BiomarkerPanelItem({ name, href, description, type }: BiomarkerPanelItemProps) { | ||
return ( | ||
<StackTemplate> | ||
<BodyCell {...desktopConfig.name} aria-label="Name"> | ||
<InternalLink href={href}>{name}</InternalLink> | ||
</BodyCell> | ||
<BodyCell {...desktopConfig.description} aria-label="Description"> | ||
<LineClamp lines={2}>{description}</LineClamp> | ||
</BodyCell> | ||
<BodyCell {...desktopConfig.type} aria-label="Type"> | ||
<UnroundedChip variant="outlined" label={type} /> | ||
</BodyCell> | ||
</StackTemplate> | ||
); | ||
} | ||
|
||
const UnroundedFilterChip = styled(SelectableChip)(({ theme }) => ({ | ||
borderRadius: theme.spacing(1), | ||
'&.MuiChip-outlined': { | ||
borderRadius: theme.spacing(1), | ||
}, | ||
})); | ||
|
||
function BiomarkerPanelFilters() { | ||
const { toggleFilterByGenes, toggleFilterByProteins } = useBiomarkersSearchActions(); | ||
const { filterType } = useBiomarkersSearchState(); | ||
return ( | ||
<Stack direction="row" spacing={1}> | ||
<UnroundedFilterChip label="Filter by Genes" isSelected={filterType === 'gene'} onClick={toggleFilterByGenes} /> | ||
<SecondaryBackgroundTooltip title="Coming soon"> | ||
<span> | ||
<UnroundedFilterChip | ||
label="Filter by Proteins" | ||
isSelected={filterType === 'protein'} | ||
onClick={toggleFilterByProteins} | ||
disabled | ||
/> | ||
</span> | ||
</SecondaryBackgroundTooltip> | ||
</Stack> | ||
); | ||
} | ||
|
||
const BiomarkerPanel = { | ||
Header: BiomarkerHeaderPanel, | ||
Item: BiomarkerPanelItem, | ||
Filters: BiomarkerPanelFilters, | ||
}; | ||
|
||
export default BiomarkerPanel; |
92 changes: 92 additions & 0 deletions
92
context/app/static/js/components/biomarkers/BiomarkersPanelList.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
import React, { useCallback, useMemo } from 'react'; | ||
import PanelList from 'js/shared-styles/panels/PanelList'; | ||
|
||
import LoadingButton from '@mui/lab/LoadingButton'; | ||
import Skeleton from '@mui/material/Skeleton'; | ||
|
||
import { PanelProps } from 'js/shared-styles/panels/Panel'; | ||
import { useSnackbarActions } from 'js/shared-styles/snackbars'; | ||
import { DownIcon } from 'js/shared-styles/icons'; | ||
import { useResultsList, useViewMore } from './hooks'; | ||
import BiomarkerPanel from './BiomarkersPanelItem'; | ||
|
||
const skeletons: PanelProps[] = Array.from({ length: 10 }).map((_, index) => ({ | ||
key: `skeleton-${index}`, | ||
children: <Skeleton width="100%" height={32} variant="rounded" key={Math.random()} />, | ||
})); | ||
|
||
function ViewMoreButton() { | ||
const { isLoading, isValidating, hasMore } = useResultsList(); | ||
const viewMore = useViewMore(); | ||
const { toastError } = useSnackbarActions(); | ||
|
||
const handleViewMore = useCallback(() => { | ||
viewMore().catch((error: unknown) => { | ||
console.error(error); | ||
toastError('Failed to load more biomarkers.'); | ||
}); | ||
}, [toastError, viewMore]); | ||
|
||
return ( | ||
<LoadingButton | ||
color="primary" | ||
variant="contained" | ||
fullWidth | ||
loading={isLoading || isValidating} | ||
onClick={handleViewMore} | ||
disabled={!hasMore} | ||
endIcon={<DownIcon />} | ||
> | ||
View More | ||
</LoadingButton> | ||
); | ||
} | ||
|
||
export default function BiomarkersPanelList() { | ||
const { genesList, isLoading } = useResultsList(); | ||
|
||
const panelsProps: PanelProps[] = useMemo(() => { | ||
if (!genesList.length) { | ||
if (isLoading) return skeletons; | ||
return [ | ||
{ | ||
children: <>No results found. Try searching for a different biomarker.</>, | ||
key: 'no-results', | ||
}, | ||
]; | ||
} | ||
const propsList: PanelProps[] = [ | ||
{ | ||
key: 'filters', | ||
noHover: true, | ||
children: <BiomarkerPanel.Filters />, | ||
}, | ||
{ | ||
key: 'header', | ||
noPadding: true, | ||
children: <BiomarkerPanel.Header />, | ||
}, | ||
...genesList.map(({ approved_name, approved_symbol, summary }) => ({ | ||
key: approved_symbol, | ||
noPadding: true, | ||
noHover: false, | ||
children: ( | ||
<BiomarkerPanel.Item | ||
name={`${approved_name} (${approved_symbol})`} | ||
description={summary || 'No description available.'} | ||
href={`/genes/${approved_symbol}`} | ||
type="Gene" | ||
/> | ||
), | ||
})), | ||
{ | ||
key: 'view-more', | ||
noPadding: true, | ||
children: <ViewMoreButton />, | ||
}, | ||
]; | ||
return propsList; | ||
}, [genesList, isLoading]); | ||
|
||
return <PanelList panelsProps={panelsProps} />; | ||
} |
23 changes: 23 additions & 0 deletions
23
context/app/static/js/components/biomarkers/BiomarkersSearchBar.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
import React from 'react'; | ||
import SearchBar from 'js/shared-styles/inputs/SearchBar'; | ||
import { useBiomarkersSearchActions, useBiomarkersSearchState } from './BiomarkersSearchContext'; | ||
|
||
// Once protein search is implemented, this should be changed to: | ||
// const searchbarPlaceholder = | ||
// 'Search for biomarkers by gene symbol, gene name or protein name. Example: CD4, Cytokeratin'; | ||
|
||
const searchbarPlaceholder = | ||
'Search for biomarkers by gene symbol. Note that searches are case-sensitive. Example: CD4, MMRN1'; | ||
|
||
export default function BiomarkersSearchBar() { | ||
const { search } = useBiomarkersSearchState(); | ||
const { setSearch } = useBiomarkersSearchActions(); | ||
return ( | ||
<SearchBar | ||
sx={{ mb: 2, width: '100%' }} | ||
placeholder={searchbarPlaceholder} | ||
value={search} | ||
onChange={(e) => setSearch(e.target.value)} | ||
/> | ||
); | ||
} |
59 changes: 59 additions & 0 deletions
59
context/app/static/js/components/biomarkers/BiomarkersSearchContext.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
import React, { Dispatch, PropsWithChildren, useMemo, useState } from 'react'; | ||
import { createContext, useContext } from 'js/helpers/context'; | ||
|
||
type FilterType = 'gene' | 'protein'; | ||
|
||
interface BiomarkersSearchStateContext { | ||
search: string; | ||
filterType?: FilterType; | ||
} | ||
|
||
interface BiomarkersSearchActionsContext { | ||
setSearch: Dispatch<React.SetStateAction<string>>; | ||
toggleFilterByGenes: () => void; | ||
toggleFilterByProteins: () => void; | ||
} | ||
|
||
const BiomarkersSearchStateContext = createContext<BiomarkersSearchStateContext>('BiomarkersSearchContext'); | ||
const BiomarkersSearchActionsContext = createContext<BiomarkersSearchActionsContext>('BiomarkersSearchActionsContext'); | ||
|
||
function BiomarkersSearchProvider({ children }: PropsWithChildren) { | ||
const [search, setSearch] = useState(''); | ||
const [filterType, setFilterType] = useState<FilterType>(); | ||
|
||
const searchState = useMemo( | ||
() => ({ | ||
search, | ||
filterType, | ||
}), | ||
[search, filterType], | ||
); | ||
|
||
const searchActions = useMemo( | ||
() => ({ | ||
setSearch, | ||
toggleFilterByGenes: () => setFilterType((c) => (c === 'gene' ? undefined : 'gene')), | ||
toggleFilterByProteins: () => setFilterType((c) => (c === 'protein' ? undefined : 'protein')), | ||
}), | ||
[], | ||
); | ||
|
||
return ( | ||
<BiomarkersSearchStateContext.Provider value={searchState}> | ||
<BiomarkersSearchActionsContext.Provider value={searchActions}> | ||
{children} | ||
</BiomarkersSearchActionsContext.Provider> | ||
</BiomarkersSearchStateContext.Provider> | ||
); | ||
} | ||
|
||
function useBiomarkersSearchState() { | ||
return useContext(BiomarkersSearchStateContext); | ||
} | ||
|
||
function useBiomarkersSearchActions() { | ||
return useContext(BiomarkersSearchActionsContext); | ||
} | ||
|
||
export default BiomarkersSearchProvider; | ||
export { useBiomarkersSearchState, useBiomarkersSearchActions }; |
Oops, something went wrong.