diff --git a/.editorconfig b/.editorconfig deleted file mode 100644 index 90913b4b9..000000000 --- a/.editorconfig +++ /dev/null @@ -1,30 +0,0 @@ -# http://editorconfig.org - -# A special property that should be specified at the top of the file outside of -# any sections. Set to true to stop .editor config file search on current file -root = true - -[*] -# Indentation style -# Possible values - tab, space -indent_style = space - -# Indentation size in single-spaced characters -# Possible values - an integer, tab -indent_size = 2 - -# Line ending file format -# Possible values - lf, crlf, cr -end_of_line = lf - -# File character encoding -# Possible values - latin1, utf-8, utf-16be, utf-16le -charset = utf-8 - -# Denotes whether to trim whitespace at the end of lines -# Possible values - true, false -trim_trailing_whitespace = true - -# Denotes whether file should end with a newline -# Possible values - true, false -insert_final_newline = true diff --git a/.eslintrc.json b/.eslintrc.json deleted file mode 100644 index 96ba9dcc1..000000000 --- a/.eslintrc.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "root": true, - "plugins": ["cypress"], - "extends": ["next/core-web-vitals", "plugin:cypress/recommended", "prettier"], - "rules": { - "linebreak-style": ["error", "unix"], - "quotes": ["error", "single"], - "semi": ["error", "never"] - }, - "parserOptions": { - "ecmaVersion": "latest", - "sourceType": "module" - }, - "ignorePatterns": ["node_modules", "components/vendor", "static"] -} diff --git a/.prettierrc b/.prettierrc deleted file mode 100644 index b52a92b10..000000000 --- a/.prettierrc +++ /dev/null @@ -1,5 +0,0 @@ -{ - "singleQuote": true, - "semi": false, - "printWidth": 100 -} diff --git a/Todo.md b/Todo.md deleted file mode 100644 index 0749200ca..000000000 --- a/Todo.md +++ /dev/null @@ -1,37 +0,0 @@ -# Frontend TODO - -## Major - -Charts: - -- Download speed by time chart component - -# Backend TODO - -## Country page - -* Number of networks that showed the presence of a middlebox - -* If Instant messagging apps might be blocked - -* If Circumvention tools are working - -* Types of blocked websites (as in the number of sites per category found to be blocked) - -* Time series of number measurements per test class (websites, im, middleboxes, etc.) - -* Time series of number of measurements per ASN by test class - -* Total number of measurements for the given country - -* Total number of network tested for a given country - -* Median upload and download speed - -* Dash measurements that show testers can stream more than a certain type of video quality - -* IM app blocking status grouped by ASN - -* Middlebox presence status grouped by ASN - -* Anomaly, measurements, and blocked counts per website. (Perhaps break this down per ASN too) diff --git a/biome.json b/biome.json new file mode 100644 index 000000000..d360f8c12 --- /dev/null +++ b/biome.json @@ -0,0 +1,25 @@ +{ + "$schema": "https://biomejs.dev/schemas/1.6.1/schema.json", + "organizeImports": { + "enabled": true + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true + } + }, + "formatter": { + "indentStyle": "space", + "indentWidth": 2 + }, + "javascript": { + "formatter": { + "semicolons": "asNeeded", + "quoteStyle": "single" + } + }, + "files": { + "ignore": [".next/", "scripts/", "public/"] + } +} diff --git a/components/Badge.js b/components/Badge.js index 0c01893cf..9e2b89d9a 100644 --- a/components/Badge.js +++ b/components/Badge.js @@ -1,10 +1,10 @@ import { Box, Flex, Text, theme } from 'ooni-components' +import * as icons from 'ooni-components/icons' import PropTypes from 'prop-types' import { cloneElement } from 'react' import { FormattedMessage } from 'react-intl' import styled from 'styled-components' import { getTestMetadata } from './utils' -import * as icons from 'ooni-components/icons' // XXX replace what is inside of search/results-list.StyledResultTag export const Badge = styled(Box)` @@ -15,7 +15,8 @@ export const Badge = styled(Box)` font-size: 12px; text-transform: uppercase; background-color: ${(props) => props.bg || props.theme.colors.gray8}; - border: ${(props) => (props.borderColor ? `1px solid ${props.borderColor}` : 'none')}; + border: ${(props) => + props.borderColor ? `1px solid ${props.borderColor}` : 'none'}; color: ${(props) => props.color || props.theme.colors.white}; letter-spacing: 1.25px; font-weight: 600; @@ -43,7 +44,11 @@ export const CategoryBadge = ({ categoryCode }) => { } return ( - + diff --git a/components/BlockText.js b/components/BlockText.js index a38426390..349a06907 100644 --- a/components/BlockText.js +++ b/components/BlockText.js @@ -1,9 +1,9 @@ -import styled from 'styled-components' import { Box } from 'ooni-components' +import styled from 'styled-components' const BlockText = styled(Box)` - background-color: ${props => props.theme.colors.gray0}; - border-left: 10px solid ${props => props.theme.colors.blue5}; + background-color: ${(props) => props.theme.colors.gray0}; + border-left: 10px solid ${(props) => props.theme.colors.blue5}; ` BlockText.defaultProps = { @@ -11,4 +11,4 @@ BlockText.defaultProps = { fontSize: 1, } -export default BlockText \ No newline at end of file +export default BlockText diff --git a/components/CallToActionBox.js b/components/CallToActionBox.js index b51f5ec13..2de39d0c5 100644 --- a/components/CallToActionBox.js +++ b/components/CallToActionBox.js @@ -2,22 +2,18 @@ import NLink from 'next/link' import { Box, Button, Flex, Heading, Text } from 'ooni-components' import { FormattedMessage } from 'react-intl' -const CallToActionBox = ({title, text}) => { +const CallToActionBox = ({ title, text }) => { return ( - + - - {title} - - - {text} - + {title} + {text} - + - + @@ -26,4 +22,4 @@ const CallToActionBox = ({title, text}) => { ) } -export default CallToActionBox \ No newline at end of file +export default CallToActionBox diff --git a/components/Chart.js b/components/Chart.js index 9be643ec3..b3cc1fb3d 100644 --- a/components/Chart.js +++ b/components/Chart.js @@ -1,4 +1,6 @@ -import GridChart, { prepareDataForGridChart } from 'components/aggregation/mat/GridChart' +import GridChart, { + prepareDataForGridChart, +} from 'components/aggregation/mat/GridChart' import { MATContextProvider } from 'components/aggregation/mat/MATContext' import { DetailsBox } from 'components/measurement/DetailsBox' import NLink from 'next/link' @@ -23,51 +25,69 @@ export const MATLink = ({ query }) => { const showMATButton = !Array.isArray(query.test_name) return ( - + - {showMATButton && + {showMATButton && ( - {intl.formatMessage({id: 'MAT.Charts.SeeOnMAT'})} + {intl.formatMessage({ id: 'MAT.Charts.SeeOnMAT' })}{' '} + - } + )} - + - {intl.formatMessage({id: 'MAT.Charts.DownloadJSONData'})} + {intl.formatMessage({ id: 'MAT.Charts.DownloadJSONData' })}{' '} + - {intl.formatMessage({id: 'MAT.Charts.DownloadCSVData'})} + {intl.formatMessage({ id: 'MAT.Charts.DownloadCSVData' })}{' '} + ) } -const Chart = React.memo(function Chart({testGroup = null, queryParams = {}, setState}) { +const Chart = React.memo(function Chart({ + testGroup = null, + queryParams = {}, + setState, +}) { const apiQuery = useMemo(() => { const qs = new URLSearchParams(queryParams).toString() return qs }, [queryParams]) - const { data, error } = useSWR( - apiQuery, - MATFetcher, - swrOptions - ) + const { data, error } = useSWR(apiQuery, MATFetcher, swrOptions) const [chartData, rowKeys, rowLabels] = useMemo(() => { if (!data) { return [null, 0] } - let chartData = data.data + const chartData = data.data const graphQuery = queryParams - const [reshapedData, rowKeys, rowLabels] = prepareDataForGridChart(chartData, graphQuery) + const [reshapedData, rowKeys, rowLabels] = prepareDataForGridChart( + chartData, + graphQuery, + ) return [reshapedData, rowKeys, rowLabels] }, [data, queryParams]) - useEffect(()=> { + useEffect(() => { if (setState && data?.data) setState(data.data) }, [data, setState]) @@ -76,9 +96,9 @@ const Chart = React.memo(function Chart({testGroup = null, queryParams = {}, set return ( // - + - {(!chartData && !error) ? ( + {!chartData && !error ? ( ) : ( <> @@ -91,16 +111,21 @@ const Chart = React.memo(function Chart({testGroup = null, queryParams = {}, set )} - {error && - -
- Error: {error.message} - - {JSON.stringify(error, null, 2)} - -
- }/> - } + {error && ( + +
+ + Error: {error.message} + + {JSON.stringify(error, null, 2)} +
+ + } + /> + )}
) diff --git a/components/CollapseTrigger.js b/components/CollapseTrigger.js index bf63d31f8..b2efed647 100644 --- a/components/CollapseTrigger.js +++ b/components/CollapseTrigger.js @@ -1,11 +1,11 @@ import React from 'react' -import styled from 'styled-components' import { MdExpandLess } from 'react-icons/md' +import styled from 'styled-components' export const CollapseTrigger = styled(MdExpandLess)` cursor: pointer; - background-color: ${props => props.$bg || '#ffffff'}; + background-color: ${(props) => props.$bg || '#ffffff'}; border-radius: 50%; - transform: ${props => props.$open ? 'rotate(0deg)': 'rotate(180deg)'}; + transform: ${(props) => (props.$open ? 'rotate(0deg)' : 'rotate(180deg)')}; transition: transform 0.1s linear; ` diff --git a/components/CountryBox.js b/components/CountryBox.js index 656d064da..8fc7f4a97 100644 --- a/components/CountryBox.js +++ b/components/CountryBox.js @@ -1,35 +1,45 @@ +import Flag from 'components/Flag' +import { GridBox } from 'components/VirtualizedGrid' import { Box, Flex, Text } from 'ooni-components' import { useIntl } from 'react-intl' import { getLocalisedRegionName } from 'utils/i18nCountries' -import Flag from 'components/Flag' -import { GridBox } from 'components/VirtualizedGrid' const CountryList = ({ countries, itemsPerRow = 6, gridGap = 3 }) => { const intl = useIntl() - const gridTemplateColumns = ['1fr 1fr', '1fr 1fr', '1fr 1fr 1fr 1fr', [...Array(itemsPerRow)].map((i) => ('1fr')).join(' ')] + const gridTemplateColumns = [ + '1fr 1fr', + '1fr 1fr', + '1fr 1fr 1fr 1fr', + [...Array(itemsPerRow)].map((i) => '1fr').join(' '), + ] return ( - + {countries.map((c) => ( - - {getLocalisedRegionName(c.country, intl.locale)} + + + + + + {getLocalisedRegionName(c.country, intl.locale)} + } count={c.measurements} /> - )) - } + ))} ) } -export default CountryList \ No newline at end of file +export default CountryList diff --git a/components/DateRangePicker.js b/components/DateRangePicker.js index 45dba90dd..9bcb35df7 100644 --- a/components/DateRangePicker.js +++ b/components/DateRangePicker.js @@ -29,7 +29,7 @@ z-index: 99999; position: absolute; max-width: 300px; background-color: #ffffff; -border: 1px solid ${props => props.theme.colors.gray2}; +border: 1px solid ${(props) => props.theme.colors.gray2}; .rdp-cell { padding: 2px 0; @@ -39,7 +39,7 @@ border: 1px solid ${props => props.theme.colors.gray2}; .rdp-day_selected:focus:not([disabled]), .rdp-day_selected:active:not([disabled]), .rdp-day_selected:hover:not([disabled]) { - background-color: ${props => props.theme.colors.blue5}; + background-color: ${(props) => props.theme.colors.blue5}; } ` @@ -61,21 +61,34 @@ const Footer = ({ handleRangeSelect, range, close }) => { return ( - + + close() + }} + > + {intl.formatMessage({ id: 'DateRange.Cancel' })} + ) } -const DateRangePicker = ({handleRangeSelect, initialRange, close, ...props}) => { +const DateRangePicker = ({ + handleRangeSelect, + initialRange, + close, + ...props +}) => { const intl = useIntl() const tomorrow = addDays(new Date(), 1) const ranges = ['Today', 'LastWeek', 'LastMonth', 'LastYear'] @@ -112,25 +125,28 @@ const DateRangePicker = ({handleRangeSelect, initialRange, close, ...props}) => return en } }, [intl.locale]) - + const selectRange = (range) => { switch (range) { case 'Today': - handleRangeSelect({from: new Date(), to: tomorrow}) + handleRangeSelect({ from: new Date(), to: tomorrow }) break case 'LastWeek': - handleRangeSelect({from: sub(new Date(), {weeks: 1}) , to: tomorrow}) + handleRangeSelect({ from: sub(new Date(), { weeks: 1 }), to: tomorrow }) break case 'LastMonth': - handleRangeSelect({from: sub(new Date(), {months: 1}) , to: tomorrow}) + handleRangeSelect({ + from: sub(new Date(), { months: 1 }), + to: tomorrow, + }) break case 'LastYear': - handleRangeSelect({from: sub(new Date(), {years: 1}) , to: tomorrow}) + handleRangeSelect({ from: sub(new Date(), { years: 1 }), to: tomorrow }) break } } - const rangesList = ranges.map((range) => + const rangesList = ranges.map((range) => ( - ) - const [range, setRange] = useState({from: parse(initialRange.from, 'yyyy-MM-dd', new Date()), to: parse(initialRange.to, 'yyyy-MM-dd', new Date())}) - + }} + > + {intl.formatMessage({ id: `DateRange.${range}` })} + + )) + const [range, setRange] = useState({ + from: parse(initialRange.from, 'yyyy-MM-dd', new Date()), + to: parse(initialRange.to, 'yyyy-MM-dd', new Date()), + }) + const onSelect = (range) => { setRange(range) } @@ -151,7 +173,7 @@ const DateRangePicker = ({handleRangeSelect, initialRange, close, ...props}) => close()}> {rangesList} - toDate={tomorrow} selected={range} onSelect={onSelect} - footer={