Skip to content

Commit

Permalink
Replace calendar component on country page
Browse files Browse the repository at this point in the history
  • Loading branch information
majakomel committed Jul 16, 2024
1 parent 32d2d8c commit 6d26a5c
Show file tree
Hide file tree
Showing 4 changed files with 204 additions and 403 deletions.
22 changes: 9 additions & 13 deletions components/CallToActionBox.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,20 @@ import { FormattedMessage } from 'react-intl'

const CallToActionBox = ({title, text}) => {
return (
<Flex my={4} bg='gray3' flexWrap='wrap'>
<Box width={1} mx={4} my={2}>
<Heading h={4}>
<Flex p={3} bg='gray3' flexWrap='wrap' minHeight='180px'>
<Box mb={2}>
<Heading h={4} m={0}>
{title}
</Heading>
<Text fontSize={2}>
<Text fontSize={1}>
{text}
</Text>
</Box>
<Flex alignItems='center' mx={4} my={4} flexDirection={['column', 'row']}>
<Box mr={4} mb={[3, 0]}>
<NLink href='https://ooni.org/install'>
<Button>
<FormattedMessage id='Country.Overview.NoData.Button.InstallProbe' />
</Button>
</NLink>
</Box>
</Flex>
<NLink href='https://ooni.org/install'>
<Button>
<FormattedMessage id='Country.Overview.NoData.Button.InstallProbe' />
</Button>
</NLink>
</Flex>
)
}
Expand Down
185 changes: 185 additions & 0 deletions components/country/Calendar.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
import { ResponsiveCalendar } from '@nivo/calendar'
import CTABox from 'components/CallToActionBox'
import SpinLoader from 'components/vendor/SpinLoader'
import { Box, Flex, theme } from 'ooni-components'
import React, { useMemo, useState } from 'react'
import { FormattedMessage } from 'react-intl'
import dayjs from 'services/dayjs'
import { fetcherWithPreprocessing } from 'services/fetchers'
import { styled } from 'styled-components'
import useSWR from 'swr'
import { getRange } from 'utils'
import FormattedMarkdown from '../FormattedMarkdown'
import { useCountry } from './CountryContext'

const swrOptions = {
revalidateOnFocus: false,
dedupingInterval: 10 * 60 * 1000,
}

const prepareDataForCalendar = (data) => {
return data.map((r) => ({
value: r.measurement_count,
day: r.measurement_start_day,
}))
}

const CallToActionBox = () => {
const { countryName } = useCountry()
return (
<CTABox
title={<FormattedMessage id='Country.Overview.NoData.Title' />}
text={<FormattedMarkdown
id='Country.Overview.NoData.CallToAction'
values={{
country: countryName
}}
/>
} />
)
}

const StyledCalendar = styled.div`
height: 180px;
`
const { colors } = theme
const chartColors = [colors.blue2, colors.blue4, colors.blue5, colors.blue7]

const findColor = number => {
if (number === 0) return colors.gray1
if (number <= 50) return chartColors[0]
if (number <= 500) return chartColors[1]
if (number <= 5000) return chartColors[2]
return chartColors[3]
}

const colorLegend = [
{color: chartColors[0], range: '1-50'},
{color: chartColors[1], range: '51-100'},
{color: chartColors[2], range: '501-5000'},
{color: chartColors[3], range: '>5000'},
]

const dateRange = (startDate, endDate) => {
if (!startDate || !endDate) return
const start = new Date(new Date(startDate.getFullYear(), 0, 0, 0).setUTCHours(0, 0, 0, 0))
const end = new Date(new Date(endDate).setUTCHours(0, 0, 0, 0))
const date = new Date(start.getTime())
const dates = []

while (date <= end) {
dates.push(new Date(date).toISOString().split('T')[0])
date.setUTCDate(date.getDate() + 1)
}
return dates
}

const backfillData = data => {
const range = dateRange(new Date(data[0].day), new Date())
return range.map((r) => (data.find((d) => d.day === r) || { value: 0, day: r}))
}

const Calendar = React.memo(function Calendar({ startYear }) {
const { countryCode } = useCountry()
const today = new Date()
const currentYear = today.getFullYear()
const firstMeasurementYear = startYear ? new Date(startYear).getFullYear() : new Date(data[0].day).getFullYear()

const [ selectedYear, setSelectedYear ] = useState(currentYear)
const since = `${selectedYear}-01-01`
const until = selectedYear === currentYear ?
dayjs.utc().add(1, 'day').format('YYYY-MM-DD') :
`${selectedYear + 1}-01-01`

const yearsOptions = getRange(firstMeasurementYear, currentYear)

const { data, error, isLoading } = useSWR(
[
'/api/v1/aggregation',
{ params: {
probe_cc: countryCode,
since,
until,
axis_x: 'measurement_start_day',
},
resultKey: 'result',
preprocessFn: prepareDataForCalendar,
},
],
fetcherWithPreprocessing,
swrOptions
)

const calendarData = useMemo(() => {
if (data && data.length) {
return backfillData(data)
} else {
return []
}
},
[data]
)

return (
<Box mb={60} mt={2}>
{isLoading && <Flex height='180px' bg='gray1' alignItems='center' justifyContent='center'><SpinLoader size={3} /></Flex>}
{!!calendarData.length &&
<StyledCalendar>
<ResponsiveCalendar
data={calendarData}
from={`${selectedYear}-01-01`}
to={`${selectedYear}-12-31`}
emptyColor={colors.gray1}
colorScale={(value) => findColor(value)}
margin={{ top: 20, right: 0, bottom: 0, left: 20 }}
monthBorderColor="#ffffff"
dayBorderWidth={2}
dayBorderColor="#ffffff"
/>
</StyledCalendar>
}
{!calendarData.length && !isLoading && <CallToActionBox />}
{error &&
<Flex height='180px' bg='gray1' p={3}>
Error: {JSON.stringify(error)}
</Flex>
}
<Flex justifyContent='space-between' alignItems='center' flexWrap={['wrap', 'wrap', 'nowrap']} sx={{gap: [1, 1, 0]}}>
<Flex>
{colorLegend.map(item => (
<span
key={item.color}
style={{marginRight: '16px'}}
>
<span style={{
width: '11px',
height: '11px',
backgroundColor: item.color,
display: 'inline-block',
marginRight: '3px',
}}></span>
{item.range}
</span>
))}
</Flex>
<Flex sx={{rowGap: 1, columnGap: 3}} flexWrap='wrap'>
{yearsOptions.map(year => (
<span
key={year}
style={{
display: 'inline-block',
cursor: 'pointer',
fontWeight: year === selectedYear ? '800' : '400'
}}
onClick={() => setSelectedYear(year)}
>
{year}
</span>
))}
</Flex>
</Flex>
</Box>
)
})

export default Calendar
121 changes: 10 additions & 111 deletions components/country/Overview.js
Original file line number Diff line number Diff line change
@@ -1,70 +1,12 @@
import React from 'react'
import { defineMessages, FormattedMessage, useIntl } from 'react-intl'
import styled from 'styled-components'
import BlockText from 'components/BlockText'
import Calendar from 'components/country/Calendar'
import { Box, Heading, Link, Text } from 'ooni-components'
import SectionHeader from './SectionHeader'
import { BoxWithTitle } from './boxes'
import TestsByGroup from './OverviewCharts'
import React from 'react'
import { FormattedMessage, useIntl } from 'react-intl'
import FormattedMarkdown from '../FormattedMarkdown'
import { useCountry } from './CountryContext'
import BlockText from 'components/BlockText'

const NwInterferenceStatus = styled(Box)`
color: ${props => props.color || props.theme.colors.gray5};
font-size: 18px;
`
NwInterferenceStatus.defaultProps = {
mb: 3
}

const messages = defineMessages({
middleboxNoData: {
id: 'Country.Overview.NwInterference.Middleboxes.NoData',
defaultMessage: ''
},
middleboxBlocked: {
id: 'Country.Overview.NwInterference.Middleboxes.Blocked',
defaultMessage: ''
},
middleboxNormal: {
id: 'Country.Overview.NwInterference.Middleboxes.Normal',
defaultMessage: ''
},
imNoData: {
id: 'Country.Overview.NwInterference.IM.NoData',
defaultMessage: ''
},
imBlocked: {
id: 'Country.Overview.NwInterference.IM.Blocked',
defaultMessage: ''
},
imNormal: {
id: 'Country.Overview.NwInterference.IM.Normal',
defaultMessage: ''
},
websitesNoData: {
id: 'Country.Overview.NwInterference.Websites.NoData',
defaultMessage: ''
},
websitesBlocked: {
id: 'Country.Overview.NwInterference.Websites.Blocked',
defaultMessage: ''
},
websitesNormal: {
id: 'Country.Overview.NwInterference.Websites.Normal',
defaultMessage: ''
}
})

const getStatus = (count, formattedMessageId)=> {
if (count === null) {
return messages[`${formattedMessageId}NoData`]
} else if (count > 0) {
return messages[`${formattedMessageId}Blocked`]
} else {
return messages[`${formattedMessageId}Normal`]
}
}
import SectionHeader from './SectionHeader'
import { BoxWithTitle } from './boxes'

const ooniBlogBaseURL = 'https://ooni.org'

Expand All @@ -76,36 +18,16 @@ const FeaturedArticle = ({link, title}) => (
</Box>
)

// const SummaryText = styled(Box)`
// border: 1px solid ${props => props.theme.colors.gray4};
// border-left: 12px solid ${props => props.theme.colors.blue5};
// font-size: 22px;
// font-style: italic;
// line-height: 1.5;
// `

// SummaryText.defaultProps = {
// p: 3,
// }

const LOW_DATA_THRESHOLD = 10

const Overview = ({
countryName,
testCoverage,
networkCoverage,
fetchTestCoverageData,
middleboxCount,
imCount,
circumventionTools,
blockedWebsitesCount,
networkCount,
measurementCount,
measuredSince,
featuredArticles = []
}) => {
const intl = useIntl()
const { countryCode } = useCountry()

return (
<>
<SectionHeader>
Expand All @@ -127,38 +49,15 @@ const Overview = ({
</BlockText>
{/* </SummaryText> */}

{/*
<BoxWithTitle title={<FormattedMessage id='Country.Overview.Heading.NwInterference' />}>
<Flex flexWrap='wrap'>
<NwInterferenceStatus width={[1, 1/3]} color={middleboxCount && 'violet8'}>
<NettestGroupMiddleBoxes size={32} />
{intl.formatMessage(getStatus(middleboxCount, 'middlebox'))}
</NwInterferenceStatus>
<NwInterferenceStatus width={[1, 1/2]} color={circumventionTools && 'pink6'}>
{intl.formatMessage(getStatus(circumventionTools, 'circumvention'))}
</NwInterferenceStatus>
<NwInterferenceStatus width={[1, 1/3]} color={imCount && 'yellow9'}>
<NettestGroupInstantMessaging size={32} />
{intl.formatMessage(getStatus(imCount, 'im'))}
</NwInterferenceStatus>
<NwInterferenceStatus width={[1, 1/3]} color={blockedWebsitesCount && 'indigo5'}>
<NettestGroupWebsites size={32} />
{intl.formatMessage(getStatus(blockedWebsitesCount, 'websites'))}
</NwInterferenceStatus>
</Flex>
</BoxWithTitle>
*/}
<Heading h={4} my={2}>
<FormattedMessage id='Country.Overview.Heading.TestsByClass' />
</Heading>
<Text fontSize={16}>
<FormattedMarkdown id='Country.Overview.Heading.TestsByClass.Description' />
</Text>
<TestsByGroup
fetchTestCoverageData={fetchTestCoverageData}
testCoverage={testCoverage}
networkCoverage={networkCoverage}
/>

<Calendar startYear={measuredSince} />

<BoxWithTitle title={<FormattedMessage id='Country.Overview.FeaturedResearch' />}>
{
(featuredArticles.length === 0)
Expand Down
Loading

0 comments on commit 6d26a5c

Please sign in to comment.