Skip to content
Draft
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
60 changes: 60 additions & 0 deletions web/src/components/Common/ChartContainer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import React, { useMemo } from 'react'
import { useResizeDetector } from 'react-resize-detector'
import styled from 'styled-components'
import type { ResizeDetectorProps } from 'react-resize-detector'
import { useInView } from 'react-intersection-observer'
import { theme } from 'src/theme'

export interface ChartContainerDimensions {
width: number
height: number
}

export interface ChartContainerProps {
resizeOptions?: ResizeDetectorProps<HTMLDivElement>
children: (dimensions: ChartContainerDimensions) => React.ReactNode
}

export function ChartContainer({ children, resizeOptions }: ChartContainerProps) {
const { width, ref: resizeRef } = useResizeDetector({
handleWidth: true,
refreshRate: 100,
refreshMode: 'debounce',
skipOnMount: false,
...resizeOptions,
})

const { inView, ref: intersectionRef } = useInView({
rootMargin: '500px',
initialInView: false,
fallbackInView: true,
// triggerOnce: true,
})

const dimensions = useMemo(() => ({ width: width ?? 0, height: (width ?? 0) / theme.plot.aspectRatio }), [width])

const childrenWithDims = useMemo(() => {
if (!inView) {
return null
}
return children(dimensions)
}, [children, dimensions, inView])

return (
<PlotWrapper ref={resizeRef}>
<PlotWrapperInner ref={intersectionRef}>{childrenWithDims}</PlotWrapperInner>
</PlotWrapper>
)
}

const PlotWrapper = styled.div`
flex: 1;
width: 100%;
`

const PlotWrapperInner = styled.div`
display: flex;
flex: 1;
width: 100%;
aspect-ratio: ${(props) => props.theme.plot.aspectRatio};
`
25 changes: 10 additions & 15 deletions web/src/components/Common/DateSlider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,14 @@ import { Theme } from 'src/theme'
import { localeAtom } from 'src/state/locale.state'

export interface DateSliderProps {
minTimestamp: number
maxTimestamp: number
initialTimestampRange: number[]
range: [number, number]
initialRange: [number, number]
fullRange: [number, number]
marks: Record<string | number, ReactNode | MarkObj>
onDateRangeChange(range: number | number[]): void
}

export function DateSlider({
minTimestamp,
maxTimestamp,
initialTimestampRange,
marks,
onDateRangeChange,
}: DateSliderProps) {
export function DateSlider({ range, fullRange, initialRange, marks, onDateRangeChange }: DateSliderProps) {
const theme = useTheme()
const locale = useRecoilValue(localeAtom)

Expand All @@ -41,23 +35,24 @@ export function DateSlider({
return (
<DateSliderOuter>
<DateSliderTextWrapper>
<DateSliderText>{formatDateHumanely(locale)(minTimestamp)}</DateSliderText>
<DateSliderText>{formatDateHumanely(locale)(range[0])}</DateSliderText>
</DateSliderTextWrapper>
<DateSliderWrapper>
<Slider
range
min={minTimestamp}
max={maxTimestamp}
min={fullRange[0]}
max={fullRange[1]}
marks={marks}
defaultValue={initialTimestampRange}
value={range}
defaultValue={initialRange}
onChange={onChange}
step={null}
allowCross={false}
{...dateSliderStyle}
/>
</DateSliderWrapper>
<DateSliderTextWrapper>
<DateSliderText>{formatDateHumanely(locale)(maxTimestamp)}</DateSliderText>
<DateSliderText>{formatDateHumanely(locale)(range[1])}</DateSliderText>
</DateSliderTextWrapper>
</DateSliderOuter>
)
Expand Down
35 changes: 35 additions & 0 deletions web/src/components/Common/FadeIn.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// https://www.joshwcomeau.com/snippets/react-components/fade-in/

import React, { PropsWithChildren } from 'react'
import styled, { keyframes } from 'styled-components'

export interface FadeInProps {
duration?: number
delay?: number
}

export function FadeIn({ duration = 300, delay = 0, children }: PropsWithChildren<FadeInProps>) {
return (
<Wrapper $delay={delay} $duration={duration}>
{children}
</Wrapper>
)
}

const fadeIn = keyframes`
from {
opacity: 0;
}
to {
opacity: 1;
}
`

const Wrapper = styled.div<{ $delay?: number; $duration?: number }>`
@media (prefers-reduced-motion: no-preference) {
animation-name: ${fadeIn};
animation-fill-mode: backwards;
animation-delay: ${(props) => props.$delay}ms;
animation-duration: ${(props) => props.$duration}ms;
}
`
24 changes: 24 additions & 0 deletions web/src/components/Layout/NavigationBreadcrumb.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import type { DropdownOption } from 'src/components/Common/DropdownWithSearch'

const paths = [
'/pathogen/:pathogenName',
'/pathogen/:pathogenName/variants',
'/pathogen/:pathogenName/regions',
'/pathogen/:pathogenName/variants/:variant',
'/pathogen/:pathogenName/regions/:region',
]
Expand Down Expand Up @@ -70,6 +72,20 @@ export function NavigationBreadcrumb() {
<PathogenName pathogenName={pathogenName} />
</BreadcrumbLink>,
)
} else if (path === '/pathogen/:pathogenName/variants' && !isNil(pathogenName)) {
segments.push(
<BreadcrumbLink key={pathogenName} href={urljoin('pathogen', pathogenName)}>
<PathogenName pathogenName={pathogenName} />
</BreadcrumbLink>,
<BreadcrumbText key="Variants">{t('Variants')}</BreadcrumbText>,
)
} else if (path === '/pathogen/:pathogenName/regions' && !isNil(pathogenName)) {
segments.push(
<BreadcrumbLink key={pathogenName} href={urljoin('pathogen', pathogenName)}>
<PathogenName pathogenName={pathogenName} />
</BreadcrumbLink>,
<BreadcrumbText key="Regions">{t('Regions')}</BreadcrumbText>,
)
} else if (path === '/pathogen/:pathogenName/variants/:variant' && !isNil(pathogenName) && !isNil(variant)) {
segments.push(
<BreadcrumbLink key={pathogenName} href={urljoin('pathogen', pathogenName)}>
Expand Down Expand Up @@ -103,6 +119,14 @@ export function NavigationBreadcrumb() {
return <span>{segmentsAndArrows}</span>
}

export function BreadcrumbText({ children }: PropsWithChildren) {
return (
<NavItem>
<NavLink tag={'span'}>{children}</NavLink>
</NavItem>
)
}

export interface BreadcrumbLinkProps extends PropsWithChildren {
href: string
}
Expand Down
8 changes: 4 additions & 4 deletions web/src/components/Loading/Loading.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,9 @@ export const LOADING = <Loading />

export function Spinner() {
return (
<div className="d-flex">
<div className="mx-auto">
<OvalLoader color="#777" height={100} width={50} />
<div className="d-flex flex-1 w-100 h-100">
<div className="m-auto">
<OvalLoader color="#777" height={100} width={30} />
</div>
</div>
)
Expand All @@ -62,7 +62,7 @@ export const SPINNER = <Spinner />
export function ThreeDots() {
return (
<div className="d-flex">
<div className="mx-auto">
<div className="m-auto">
<ThreeDotsLoader color="#777" height={100} width={50} />
</div>
</div>
Expand Down
9 changes: 8 additions & 1 deletion web/src/components/Pathogen/PathogenPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import styled from 'styled-components'
import urljoin from 'url-join'
import { useTranslationSafe } from 'src/helpers/useTranslationSafe'
import { useCountries, useRegions, useVariantsDataQuery, useVariantStyle } from 'src/io/getData'
import { LinkSmart } from 'src/components/Link/LinkSmart'
import { ColoredBox } from 'src/components/Common/ColoredBox'
import { GeoIconContinent } from 'src/components/Common/GeoIconContinent'
import { GeoIconCountry } from 'src/components/Common/GeoIconCountry'
Expand Down Expand Up @@ -46,7 +47,7 @@ export interface PathogenPageProps {
pathogenName: string
}

export function PathogenPage({ pathogenName }: PathogenPageProps) {
export default function PathogenPage({ pathogenName }: PathogenPageProps) {
return (
<PageContainer>
<ScreenHeightRow noGutters>
Expand Down Expand Up @@ -171,6 +172,9 @@ export function ListOfRegions({ pathogenName, ...restProps }: ListOfRegionsProps
<Row noGutters className="d-flex w-100">
<Col>
<h3 className="mx-2">{t('Regions')}</h3>
<LinkSmart className="mx-2" href={`/pathogen/${pathogenName}/regions`}>
{t('Compare')}
</LinkSmart>
</Col>
<Col md={6} className="ml-auto">
<SearchBox searchTitle={'Search regions'} searchTerm={searchTerm} onSearchTermChange={setSearchTerm} />
Expand Down Expand Up @@ -206,6 +210,9 @@ export function ListOfVariants({ pathogenName, ...restProps }: ListOfVariantsPro
<Row noGutters className="d-flex w-100">
<Col>
<h3 className="mx-2">{t('Variants')}</h3>
<LinkSmart className="mx-2" href={`/pathogen/${pathogenName}/variants`}>
{t('Compare')}
</LinkSmart>
</Col>
<Col md={6} className="ml-auto">
<SearchBox searchTitle={'Search variants'} searchTerm={searchTerm} onSearchTermChange={setSearchTerm} />
Expand Down
2 changes: 1 addition & 1 deletion web/src/components/Regions/RegionPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export interface RegionsPageProps {
location: string
}

export function RegionPage({ pathogenName, location }: RegionsPageProps) {
export default function RegionPage({ pathogenName, location }: RegionsPageProps) {
const { t } = useTranslationSafe()
const pathogen = usePathogen(pathogenName)
const { regions, countries } = useRegionsDataQuery(pathogen.name)
Expand Down
53 changes: 53 additions & 0 deletions web/src/components/Regions/RegionsPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import React from 'react'
import { Col, Row } from 'reactstrap'
import styled from 'styled-components'
import { PageContainerHorizontal } from 'src/components/Layout/PageContainer'
import { useTranslationSafe } from 'src/helpers/useTranslationSafe'
import { usePathogen } from 'src/io/getData'
import { VariantsAndRegionsSidebar } from 'src/components/Sidebar/VariantsAndRegionsSidebar'
import { RegionsPlotMulti } from 'src/components/Regions/RegionsPlotMulti'

export interface RegionsPageProps {
pathogenName: string
}

export default function RegionsPage({ pathogenName }: RegionsPageProps) {
const { t } = useTranslationSafe()
const pathogen = usePathogen(pathogenName)

return (
<PageContainerHorizontal>
<VariantsAndRegionsSidebar pathogenName={pathogenName} />

<MainContent>
<MainContentInner>
<Row noGutters>
<Col>
<span className="d-flex w-100 mt-2">
<h4 className="mx-auto text-center">
{t('{{pathogen}} by region', { pathogen: t(pathogen.nameFriendly) })}
</h4>
</span>
</Col>
</Row>

<RegionsPlotMulti pathogen={pathogen} />
</MainContentInner>
</MainContent>
</PageContainerHorizontal>
)
}

const MainContent = styled.div`
display: flex;
flex-direction: row;
flex: 1 1 100%;
overflow: hidden;
`

const MainContentInner = styled.div`
display: flex;
flex-direction: column;
flex: 1 1 100%;
overflow: hidden;
`
Loading