From c6e2b7d22810e6cbe83fe26357055331327cb330 Mon Sep 17 00:00:00 2001 From: majakomel Date: Wed, 17 Jul 2024 10:30:27 +0200 Subject: [PATCH] Use Tailwind for styling --- components/Badge.js | 53 +- components/BlockText.js | 20 +- components/ButtonSpinner.js | 13 +- components/CallToActionBox.js | 33 +- components/Chart.js | 125 +- components/CollapseTrigger.js | 11 - components/CountryBox.js | 31 +- components/DateRangePicker.js | 154 +- components/DomainChart.js | 74 +- components/Flag.js | 316 +- components/Footer.js | 178 +- components/FormattedMarkdown.js | 38 +- components/GridLoader.js | 81 +- components/Header.js | 58 +- components/Layout.js | 97 +- components/MATChart.js | 76 +- components/NavBar.js | 250 +- components/NotFound.js | 22 +- components/SharedStyledComponents.js | 65 +- components/SocialButtons.js | 42 +- components/TestNameOptions.js | 41 +- components/ThirdPartyDataChart.js | 185 +- components/VictoryTheme.js | 241 +- components/VirtualizedGrid.js | 126 +- components/aggregation/Debug.js | 204 +- components/aggregation/DebugContext.js | 61 +- components/aggregation/mat/ChartHeader.js | 104 +- .../aggregation/mat/ConfirmationModal.js | 61 +- .../aggregation/mat/CountryNameLabel.js | 13 +- components/aggregation/mat/CustomBarItem.js | 27 +- components/aggregation/mat/CustomTooltip.js | 122 +- components/aggregation/mat/Filters.js | 441 +-- components/aggregation/mat/Form.js | 293 +- components/aggregation/mat/FunnelChart.js | 29 +- components/aggregation/mat/GridChart.js | 77 +- components/aggregation/mat/HeatMapChart.js | 216 -- components/aggregation/mat/Help.js | 55 +- components/aggregation/mat/MATContext.js | 28 +- components/aggregation/mat/NoCharts.js | 35 +- components/aggregation/mat/RowChart.js | 148 +- components/aggregation/mat/StackedBarChart.js | 24 +- components/aggregation/mat/TableView.js | 34 +- components/aggregation/mat/VirtualRows.js | 56 +- components/aggregation/mat/XAxis.js | 27 +- components/aggregation/mat/colorMap.js | 12 +- components/aggregation/mat/computations.js | 40 +- components/aggregation/mat/labels.js | 28 +- components/aggregation/mat/timeScaleXAxis.js | 55 +- components/as/Calendar.js | 83 +- components/as/Form.js | 32 +- components/as/Loader.js | 1 - components/axios-plugins.js | 1 - components/colors.js | 12 +- components/country/Apps.js | 20 +- components/country/Calendar.js | 64 +- .../country/ConfirmedBlockedCategory.js | 102 +- components/country/CountryContext.js | 18 +- components/country/CountryDetails.js | 127 +- components/country/CountryHead.js | 59 +- components/country/Overview.js | 69 +- components/country/PageNavMenu.js | 85 +- components/country/SectionHeader.js | 46 +- components/country/Tooltip.js | 17 +- components/country/Websites.js | 57 +- components/country/boxes.js | 32 +- components/dashboard/Charts.js | 152 +- components/dashboard/Form.js | 65 +- components/dashboard/MetaTags.js | 21 +- components/domain/Form.js | 44 +- components/findings/FindingDisplay.js | 69 +- components/findings/Form.js | 304 +- components/findings/LoginRequiredModal.js | 39 +- components/flags/ad.js | 8 - components/flags/ae.js | 8 - components/flags/af.js | 8 - components/flags/ag.js | 8 - components/flags/ai.js | 8 - components/flags/al.js | 8 - components/flags/am.js | 8 - components/flags/ao.js | 8 - components/flags/aq.js | 8 - components/flags/ar.js | 8 - components/flags/as.js | 8 - components/flags/at.js | 8 - components/flags/au.js | 8 - components/flags/aw.js | 8 - components/flags/ax.js | 8 - components/flags/az.js | 8 - components/flags/ba.js | 8 - components/flags/bb.js | 8 - components/flags/bd.js | 8 - components/flags/be.js | 8 - components/flags/bf.js | 8 - components/flags/bg.js | 8 - components/flags/bh.js | 8 - components/flags/bi.js | 8 - components/flags/bj.js | 8 - components/flags/bl.js | 8 - components/flags/bm.js | 8 - components/flags/bn.js | 8 - components/flags/bo.js | 8 - components/flags/bq.js | 8 - components/flags/br.js | 8 - components/flags/bs.js | 8 - components/flags/bt.js | 8 - components/flags/bv.js | 8 - components/flags/bw.js | 8 - components/flags/by.js | 8 - components/flags/bz.js | 8 - components/flags/ca.js | 8 - components/flags/cc.js | 8 - components/flags/cd.js | 8 - components/flags/cf.js | 8 - components/flags/cg.js | 8 - components/flags/ch.js | 8 - components/flags/ci.js | 8 - components/flags/ck.js | 8 - components/flags/cl.js | 8 - components/flags/cm.js | 8 - components/flags/cn.js | 8 - components/flags/co.js | 8 - components/flags/cr.js | 8 - components/flags/cu.js | 8 - components/flags/cv.js | 8 - components/flags/cw.js | 8 - components/flags/cx.js | 8 - components/flags/cy.js | 8 - components/flags/cz.js | 8 - components/flags/de.js | 8 - components/flags/dj.js | 8 - components/flags/dk.js | 8 - components/flags/dm.js | 8 - components/flags/do.js | 8 - components/flags/dz.js | 8 - components/flags/ec.js | 8 - components/flags/ee.js | 8 - components/flags/eg.js | 8 - components/flags/eh.js | 8 - components/flags/er.js | 8 - components/flags/es.js | 8 - components/flags/et.js | 8 - components/flags/eu.js | 8 - components/flags/fi.js | 8 - components/flags/fj.js | 8 - components/flags/fk.js | 8 - components/flags/fm.js | 8 - components/flags/fo.js | 8 - components/flags/fr.js | 8 - components/flags/ga.js | 8 - components/flags/gb.js | 8 - components/flags/gd.js | 8 - components/flags/ge.js | 8 - components/flags/gf.js | 8 - components/flags/gg.js | 8 - components/flags/gh.js | 8 - components/flags/gi.js | 8 - components/flags/gl.js | 8 - components/flags/gm.js | 8 - components/flags/gn.js | 8 - components/flags/gp.js | 8 - components/flags/gq.js | 8 - components/flags/gr.js | 8 - components/flags/gs.js | 8 - components/flags/gt.js | 8 - components/flags/gu.js | 8 - components/flags/gw.js | 8 - components/flags/gy.js | 8 - components/flags/hk.js | 8 - components/flags/hm.js | 8 - components/flags/hn.js | 8 - components/flags/hr.js | 8 - components/flags/ht.js | 8 - components/flags/hu.js | 8 - components/flags/id.js | 8 - components/flags/ie.js | 8 - components/flags/il.js | 8 - components/flags/im.js | 8 - components/flags/in.js | 8 - components/flags/io.js | 8 - components/flags/iq.js | 8 - components/flags/ir.js | 8 - components/flags/is.js | 8 - components/flags/it.js | 8 - components/flags/je.js | 8 - components/flags/jm.js | 8 - components/flags/jo.js | 8 - components/flags/jp.js | 8 - components/flags/ke.js | 8 - components/flags/kg.js | 8 - components/flags/kh.js | 8 - components/flags/ki.js | 8 - components/flags/km.js | 8 - components/flags/kn.js | 8 - components/flags/kp.js | 8 - components/flags/kr.js | 8 - components/flags/kw.js | 8 - components/flags/ky.js | 8 - components/flags/kz.js | 8 - components/flags/la.js | 8 - components/flags/lb.js | 8 - components/flags/lc.js | 8 - components/flags/li.js | 8 - components/flags/lk.js | 8 - components/flags/lr.js | 8 - components/flags/ls.js | 8 - components/flags/lt.js | 8 - components/flags/lu.js | 8 - components/flags/lv.js | 8 - components/flags/ly.js | 8 - components/flags/ma.js | 8 - components/flags/mc.js | 8 - components/flags/md.js | 8 - components/flags/me.js | 8 - components/flags/mf.js | 8 - components/flags/mg.js | 8 - components/flags/mh.js | 8 - components/flags/mk.js | 8 - components/flags/ml.js | 8 - components/flags/mm.js | 8 - components/flags/mn.js | 8 - components/flags/mo.js | 8 - components/flags/mp.js | 8 - components/flags/mq.js | 8 - components/flags/mr.js | 8 - components/flags/ms.js | 8 - components/flags/mt.js | 8 - components/flags/mu.js | 8 - components/flags/mv.js | 8 - components/flags/mw.js | 8 - components/flags/mx.js | 8 - components/flags/my.js | 8 - components/flags/mz.js | 8 - components/flags/na.js | 8 - components/flags/nc.js | 8 - components/flags/ne.js | 8 - components/flags/nf.js | 8 - components/flags/ng.js | 8 - components/flags/ni.js | 8 - components/flags/nl.js | 8 - components/flags/no.js | 8 - components/flags/np.js | 8 - components/flags/nr.js | 8 - components/flags/nu.js | 8 - components/flags/nz.js | 8 - components/flags/om.js | 8 - components/flags/pa.js | 8 - components/flags/pe.js | 8 - components/flags/pf.js | 8 - components/flags/pg.js | 8 - components/flags/ph.js | 8 - components/flags/pk.js | 8 - components/flags/pl.js | 8 - components/flags/pm.js | 8 - components/flags/pn.js | 8 - components/flags/pr.js | 8 - components/flags/ps.js | 8 - components/flags/pt.js | 8 - components/flags/pw.js | 8 - components/flags/py.js | 8 - components/flags/qa.js | 8 - components/flags/re.js | 8 - components/flags/ro.js | 8 - components/flags/rs.js | 8 - components/flags/ru.js | 8 - components/flags/rw.js | 8 - components/flags/sa.js | 8 - components/flags/sb.js | 8 - components/flags/sc.js | 8 - components/flags/sd.js | 8 - components/flags/se.js | 8 - components/flags/sg.js | 8 - components/flags/sh.js | 8 - components/flags/si.js | 8 - components/flags/sj.js | 8 - components/flags/sk.js | 8 - components/flags/sl.js | 8 - components/flags/sm.js | 8 - components/flags/sn.js | 8 - components/flags/so.js | 8 - components/flags/sr.js | 8 - components/flags/ss.js | 8 - components/flags/st.js | 8 - components/flags/sv.js | 8 - components/flags/sx.js | 8 - components/flags/sy.js | 8 - components/flags/sz.js | 8 - components/flags/tc.js | 8 - components/flags/td.js | 8 - components/flags/tf.js | 8 - components/flags/tg.js | 8 - components/flags/th.js | 8 - components/flags/tj.js | 8 - components/flags/tk.js | 8 - components/flags/tl.js | 8 - components/flags/tm.js | 8 - components/flags/tn.js | 8 - components/flags/to.js | 8 - components/flags/tr.js | 8 - components/flags/tt.js | 8 - components/flags/tv.js | 8 - components/flags/tw.js | 8 - components/flags/tz.js | 8 - components/flags/ua.js | 8 - components/flags/ug.js | 8 - components/flags/um.js | 8 - components/flags/un.js | 8 - components/flags/us.js | 8 - components/flags/uy.js | 8 - components/flags/uz.js | 8 - components/flags/va.js | 8 - components/flags/vc.js | 8 - components/flags/ve.js | 8 - components/flags/vg.js | 8 - components/flags/vi.js | 8 - components/flags/vn.js | 8 - components/flags/vu.js | 8 - components/flags/wf.js | 8 - components/flags/ws.js | 8 - components/flags/ye.js | 8 - components/flags/yt.js | 8 - components/flags/za.js | 8 - components/flags/zm.js | 8 - components/flags/zw.js | 8 - components/landing/ChartLoader.js | 8 +- components/landing/HighlightBox.js | 80 +- components/landing/HighlightsSection.js | 121 +- components/landing/Stats.js | 168 +- components/login/LoginForm.js | 115 +- components/measurement/AccessPointStatus.js | 41 +- components/measurement/CommonDetails.js | 214 +- components/measurement/CommonSummary.js | 123 +- components/measurement/DetailsBox.js | 120 +- components/measurement/DetailsHeader.js | 63 +- components/measurement/FeedbackBox.js | 422 ++- components/measurement/HeadMetadata.js | 58 +- components/measurement/Hero.js | 83 +- components/measurement/InfoBoxItem.js | 28 +- .../measurement/MeasurementContainer.js | 21 +- components/measurement/PerformanceDetails.js | 87 +- components/measurement/StatusInfo.js | 26 +- components/measurement/SummaryText.js | 45 +- components/measurement/nettests/Dash.js | 141 +- components/measurement/nettests/Default.js | 6 +- .../measurement/nettests/FacebookMessenger.js | 188 +- .../nettests/HTTPHeaderFieldManipulation.js | 42 +- .../nettests/HTTPInvalidRequestLine.js | 54 +- components/measurement/nettests/Ndt.js | 152 +- components/measurement/nettests/Psiphon.js | 69 +- components/measurement/nettests/Signal.js | 41 +- components/measurement/nettests/Telegram.js | 164 +- components/measurement/nettests/Tor.js | 287 +- .../measurement/nettests/TorSnowflake.js | 87 +- components/measurement/nettests/VanillaTor.js | 84 +- .../measurement/nettests/WebConnectivity.js | 669 ++-- components/measurement/nettests/WhatsApp.js | 135 +- .../measurement/nettests/mlab_servers.json | 3220 +++++++---------- components/measurement/nettests/mlab_utils.js | 4 +- .../measurement/useTestKeyController.js | 72 - components/search/FilterSidebar.js | 152 +- components/search/FilterTabs.js | 92 - components/search/Loader.js | 16 +- components/search/ResultsList.js | 321 +- components/test-info.js | 261 +- components/useMobileDetection.js | 13 - components/utils.js | 22 +- components/utils/categoryCodes.js | 96 +- components/utils/profiler.js | 17 +- components/vendor/SpinLoader.js | 119 +- components/withIntl.js | 16 +- hooks/useFilterWithSort.js | 10 +- hooks/useScrollPosition.js | 4 +- hooks/useUser.js | 21 +- lib/api.js | 42 +- next.config.js | 6 +- package.json | 14 +- pages/404.js | 76 +- pages/_app.js | 10 +- pages/_document.js | 28 +- pages/_error.js | 2 +- pages/api/cloudflare.js | 71 +- pages/api/ioda.js | 56 +- pages/as/[probe_asn].js | 185 +- pages/chart/circumvention.js | 88 +- pages/chart/mat.js | 72 +- pages/countries.js | 230 +- pages/country/[countryCode].js | 51 +- pages/domain/[domain].js | 152 +- pages/domains.js | 185 +- pages/findings/[id].js | 47 +- pages/findings/create.js | 37 +- pages/findings/dashboard.js | 227 +- pages/findings/edit/[id].js | 50 +- pages/findings/index.js | 208 +- pages/index.js | 349 +- pages/login.js | 87 +- pages/m/[measurement_uid].js | 123 +- pages/measurement/[[...report_id]].js | 30 +- pages/networks.js | 152 +- pages/search.js | 254 +- postcss.config.js | 6 + scripts/now-preview.sh | 7 - services/dayjs.js | 8 +- services/fetchers.js | 49 +- styles/globals.css | 54 + tailwind.config.js | 49 + yarn.lock | 749 ++-- 406 files changed, 9231 insertions(+), 10968 deletions(-) delete mode 100644 components/CollapseTrigger.js delete mode 100644 components/aggregation/mat/HeatMapChart.js delete mode 100644 components/flags/ad.js delete mode 100644 components/flags/ae.js delete mode 100644 components/flags/af.js delete mode 100644 components/flags/ag.js delete mode 100644 components/flags/ai.js delete mode 100644 components/flags/al.js delete mode 100644 components/flags/am.js delete mode 100644 components/flags/ao.js delete mode 100644 components/flags/aq.js delete mode 100644 components/flags/ar.js delete mode 100644 components/flags/as.js delete mode 100644 components/flags/at.js delete mode 100644 components/flags/au.js delete mode 100644 components/flags/aw.js delete mode 100644 components/flags/ax.js delete mode 100644 components/flags/az.js delete mode 100644 components/flags/ba.js delete mode 100644 components/flags/bb.js delete mode 100644 components/flags/bd.js delete mode 100644 components/flags/be.js delete mode 100644 components/flags/bf.js delete mode 100644 components/flags/bg.js delete mode 100644 components/flags/bh.js delete mode 100644 components/flags/bi.js delete mode 100644 components/flags/bj.js delete mode 100644 components/flags/bl.js delete mode 100644 components/flags/bm.js delete mode 100644 components/flags/bn.js delete mode 100644 components/flags/bo.js delete mode 100644 components/flags/bq.js delete mode 100644 components/flags/br.js delete mode 100644 components/flags/bs.js delete mode 100644 components/flags/bt.js delete mode 100644 components/flags/bv.js delete mode 100644 components/flags/bw.js delete mode 100644 components/flags/by.js delete mode 100644 components/flags/bz.js delete mode 100644 components/flags/ca.js delete mode 100644 components/flags/cc.js delete mode 100644 components/flags/cd.js delete mode 100644 components/flags/cf.js delete mode 100644 components/flags/cg.js delete mode 100644 components/flags/ch.js delete mode 100644 components/flags/ci.js delete mode 100644 components/flags/ck.js delete mode 100644 components/flags/cl.js delete mode 100644 components/flags/cm.js delete mode 100644 components/flags/cn.js delete mode 100644 components/flags/co.js delete mode 100644 components/flags/cr.js delete mode 100644 components/flags/cu.js delete mode 100644 components/flags/cv.js delete mode 100644 components/flags/cw.js delete mode 100644 components/flags/cx.js delete mode 100644 components/flags/cy.js delete mode 100644 components/flags/cz.js delete mode 100644 components/flags/de.js delete mode 100644 components/flags/dj.js delete mode 100644 components/flags/dk.js delete mode 100644 components/flags/dm.js delete mode 100644 components/flags/do.js delete mode 100644 components/flags/dz.js delete mode 100644 components/flags/ec.js delete mode 100644 components/flags/ee.js delete mode 100644 components/flags/eg.js delete mode 100644 components/flags/eh.js delete mode 100644 components/flags/er.js delete mode 100644 components/flags/es.js delete mode 100644 components/flags/et.js delete mode 100644 components/flags/eu.js delete mode 100644 components/flags/fi.js delete mode 100644 components/flags/fj.js delete mode 100644 components/flags/fk.js delete mode 100644 components/flags/fm.js delete mode 100644 components/flags/fo.js delete mode 100644 components/flags/fr.js delete mode 100644 components/flags/ga.js delete mode 100644 components/flags/gb.js delete mode 100644 components/flags/gd.js delete mode 100644 components/flags/ge.js delete mode 100644 components/flags/gf.js delete mode 100644 components/flags/gg.js delete mode 100644 components/flags/gh.js delete mode 100644 components/flags/gi.js delete mode 100644 components/flags/gl.js delete mode 100644 components/flags/gm.js delete mode 100644 components/flags/gn.js delete mode 100644 components/flags/gp.js delete mode 100644 components/flags/gq.js delete mode 100644 components/flags/gr.js delete mode 100644 components/flags/gs.js delete mode 100644 components/flags/gt.js delete mode 100644 components/flags/gu.js delete mode 100644 components/flags/gw.js delete mode 100644 components/flags/gy.js delete mode 100644 components/flags/hk.js delete mode 100644 components/flags/hm.js delete mode 100644 components/flags/hn.js delete mode 100644 components/flags/hr.js delete mode 100644 components/flags/ht.js delete mode 100644 components/flags/hu.js delete mode 100644 components/flags/id.js delete mode 100644 components/flags/ie.js delete mode 100644 components/flags/il.js delete mode 100644 components/flags/im.js delete mode 100644 components/flags/in.js delete mode 100644 components/flags/io.js delete mode 100644 components/flags/iq.js delete mode 100644 components/flags/ir.js delete mode 100644 components/flags/is.js delete mode 100644 components/flags/it.js delete mode 100644 components/flags/je.js delete mode 100644 components/flags/jm.js delete mode 100644 components/flags/jo.js delete mode 100644 components/flags/jp.js delete mode 100644 components/flags/ke.js delete mode 100644 components/flags/kg.js delete mode 100644 components/flags/kh.js delete mode 100644 components/flags/ki.js delete mode 100644 components/flags/km.js delete mode 100644 components/flags/kn.js delete mode 100644 components/flags/kp.js delete mode 100644 components/flags/kr.js delete mode 100644 components/flags/kw.js delete mode 100644 components/flags/ky.js delete mode 100644 components/flags/kz.js delete mode 100644 components/flags/la.js delete mode 100644 components/flags/lb.js delete mode 100644 components/flags/lc.js delete mode 100644 components/flags/li.js delete mode 100644 components/flags/lk.js delete mode 100644 components/flags/lr.js delete mode 100644 components/flags/ls.js delete mode 100644 components/flags/lt.js delete mode 100644 components/flags/lu.js delete mode 100644 components/flags/lv.js delete mode 100644 components/flags/ly.js delete mode 100644 components/flags/ma.js delete mode 100644 components/flags/mc.js delete mode 100644 components/flags/md.js delete mode 100644 components/flags/me.js delete mode 100644 components/flags/mf.js delete mode 100644 components/flags/mg.js delete mode 100644 components/flags/mh.js delete mode 100644 components/flags/mk.js delete mode 100644 components/flags/ml.js delete mode 100644 components/flags/mm.js delete mode 100644 components/flags/mn.js delete mode 100644 components/flags/mo.js delete mode 100644 components/flags/mp.js delete mode 100644 components/flags/mq.js delete mode 100644 components/flags/mr.js delete mode 100644 components/flags/ms.js delete mode 100644 components/flags/mt.js delete mode 100644 components/flags/mu.js delete mode 100644 components/flags/mv.js delete mode 100644 components/flags/mw.js delete mode 100644 components/flags/mx.js delete mode 100644 components/flags/my.js delete mode 100644 components/flags/mz.js delete mode 100644 components/flags/na.js delete mode 100644 components/flags/nc.js delete mode 100644 components/flags/ne.js delete mode 100644 components/flags/nf.js delete mode 100644 components/flags/ng.js delete mode 100644 components/flags/ni.js delete mode 100644 components/flags/nl.js delete mode 100644 components/flags/no.js delete mode 100644 components/flags/np.js delete mode 100644 components/flags/nr.js delete mode 100644 components/flags/nu.js delete mode 100644 components/flags/nz.js delete mode 100644 components/flags/om.js delete mode 100644 components/flags/pa.js delete mode 100644 components/flags/pe.js delete mode 100644 components/flags/pf.js delete mode 100644 components/flags/pg.js delete mode 100644 components/flags/ph.js delete mode 100644 components/flags/pk.js delete mode 100644 components/flags/pl.js delete mode 100644 components/flags/pm.js delete mode 100644 components/flags/pn.js delete mode 100644 components/flags/pr.js delete mode 100644 components/flags/ps.js delete mode 100644 components/flags/pt.js delete mode 100644 components/flags/pw.js delete mode 100644 components/flags/py.js delete mode 100644 components/flags/qa.js delete mode 100644 components/flags/re.js delete mode 100644 components/flags/ro.js delete mode 100644 components/flags/rs.js delete mode 100644 components/flags/ru.js delete mode 100644 components/flags/rw.js delete mode 100644 components/flags/sa.js delete mode 100644 components/flags/sb.js delete mode 100644 components/flags/sc.js delete mode 100644 components/flags/sd.js delete mode 100644 components/flags/se.js delete mode 100644 components/flags/sg.js delete mode 100644 components/flags/sh.js delete mode 100644 components/flags/si.js delete mode 100644 components/flags/sj.js delete mode 100644 components/flags/sk.js delete mode 100644 components/flags/sl.js delete mode 100644 components/flags/sm.js delete mode 100644 components/flags/sn.js delete mode 100644 components/flags/so.js delete mode 100644 components/flags/sr.js delete mode 100644 components/flags/ss.js delete mode 100644 components/flags/st.js delete mode 100644 components/flags/sv.js delete mode 100644 components/flags/sx.js delete mode 100644 components/flags/sy.js delete mode 100644 components/flags/sz.js delete mode 100644 components/flags/tc.js delete mode 100644 components/flags/td.js delete mode 100644 components/flags/tf.js delete mode 100644 components/flags/tg.js delete mode 100644 components/flags/th.js delete mode 100644 components/flags/tj.js delete mode 100644 components/flags/tk.js delete mode 100644 components/flags/tl.js delete mode 100644 components/flags/tm.js delete mode 100644 components/flags/tn.js delete mode 100644 components/flags/to.js delete mode 100644 components/flags/tr.js delete mode 100644 components/flags/tt.js delete mode 100644 components/flags/tv.js delete mode 100644 components/flags/tw.js delete mode 100644 components/flags/tz.js delete mode 100644 components/flags/ua.js delete mode 100644 components/flags/ug.js delete mode 100644 components/flags/um.js delete mode 100644 components/flags/un.js delete mode 100644 components/flags/us.js delete mode 100644 components/flags/uy.js delete mode 100644 components/flags/uz.js delete mode 100644 components/flags/va.js delete mode 100644 components/flags/vc.js delete mode 100644 components/flags/ve.js delete mode 100644 components/flags/vg.js delete mode 100644 components/flags/vi.js delete mode 100644 components/flags/vn.js delete mode 100644 components/flags/vu.js delete mode 100644 components/flags/wf.js delete mode 100644 components/flags/ws.js delete mode 100644 components/flags/ye.js delete mode 100644 components/flags/yt.js delete mode 100644 components/flags/za.js delete mode 100644 components/flags/zm.js delete mode 100644 components/flags/zw.js delete mode 100644 components/measurement/useTestKeyController.js delete mode 100644 components/search/FilterTabs.js delete mode 100644 components/useMobileDetection.js create mode 100644 postcss.config.js delete mode 100755 scripts/now-preview.sh create mode 100644 styles/globals.css create mode 100644 tailwind.config.js diff --git a/components/Badge.js b/components/Badge.js index 0c01893cf..2941e6fd6 100644 --- a/components/Badge.js +++ b/components/Badge.js @@ -1,35 +1,38 @@ -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 { twMerge } from 'tailwind-merge' 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)` - display: inline-block; - border-radius: 4px; - padding: 4px 8px; - line-height: 16px; - font-size: 12px; - text-transform: uppercase; - background-color: ${(props) => props.bg || props.theme.colors.gray8}; - border: ${(props) => (props.borderColor ? `1px solid ${props.borderColor}` : 'none')}; - color: ${(props) => props.color || props.theme.colors.white}; - letter-spacing: 1.25px; - font-weight: 600; -` +// export const Badge = styled(Box)` +// background-color: ${(props) => props.bg || props.theme.colors.gray8}; +// border: ${(props) => (props.borderColor ? `1px solid ${props.borderColor}` : 'none')}; + +// ` + +export const Badge = ({ className, ...props }) => { + return ( +
+ ) +} const TestGroupBadge = ({ testName, ...props }) => { const { icon, groupName, color } = getTestMetadata(testName) return ( - - - {groupName} + +
+
{groupName}
{cloneElement(icon, { size: 12 })} - +
) } @@ -43,13 +46,13 @@ export const CategoryBadge = ({ categoryCode }) => { } return ( - - - + +
+
- +
{IconComponent && } - +
) } diff --git a/components/BlockText.js b/components/BlockText.js index a38426390..3e0321be4 100644 --- a/components/BlockText.js +++ b/components/BlockText.js @@ -1,14 +1,8 @@ -import styled from 'styled-components' -import { Box } from 'ooni-components' +const BlockText = ({ className, ...props }) => ( +
+) -const BlockText = styled(Box)` - background-color: ${props => props.theme.colors.gray0}; - border-left: 10px solid ${props => props.theme.colors.blue5}; -` - -BlockText.defaultProps = { - p: 3, - fontSize: 1, -} - -export default BlockText \ No newline at end of file +export default BlockText diff --git a/components/ButtonSpinner.js b/components/ButtonSpinner.js index f8ff7aca7..8725730ea 100644 --- a/components/ButtonSpinner.js +++ b/components/ButtonSpinner.js @@ -1,16 +1,5 @@ import { ImSpinner8 } from 'react-icons/im' -import { keyframes, styled } from 'styled-components' -const spin = keyframes` -to { - transform: rotate(360deg); -} -` - -const StyledSpinner = styled(ImSpinner8)` - animation: ${spin} 1s linear infinite; -` - -const ButtonSpinner = () => +const ButtonSpinner = () => export default ButtonSpinner diff --git a/components/CallToActionBox.js b/components/CallToActionBox.js index 42099aaaf..28751062c 100644 --- a/components/CallToActionBox.js +++ b/components/CallToActionBox.js @@ -1,25 +1,20 @@ -import NLink from 'next/link' -import { Box, Button, Flex, Heading, Text } from 'ooni-components' +import Link from 'next/link' import { FormattedMessage } from 'react-intl' -const CallToActionBox = ({title, text}) => { +const CallToActionBox = ({ title, text }) => { return ( - - - - {title} - - - {text} - - - - - - +
+
+

{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..df7d1dd4e 100644 --- a/components/Chart.js +++ b/components/Chart.js @@ -1,14 +1,14 @@ -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' -import { Box, Flex } from 'ooni-components' -import React, { useEffect, useMemo } from 'react' +import Link from 'next/link' +import { memo, useEffect, useMemo } from 'react' import { MdBarChart, MdOutlineFileDownload } from 'react-icons/md' import { FormattedMessage, useIntl } from 'react-intl' import { MATFetcher } from 'services/fetchers' import useSWR from 'swr' -import { StyledHollowButton } from './SharedStyledComponents' const swrOptions = { revalidateOnFocus: false, @@ -23,51 +23,55 @@ export const MATLink = ({ query }) => { const showMATButton = !Array.isArray(query.test_name) return ( - - - {showMATButton && - - - {intl.formatMessage({id: 'MAT.Charts.SeeOnMAT'})} - - - } - - - - {intl.formatMessage({id: 'MAT.Charts.DownloadJSONData'})} - - - {intl.formatMessage({id: 'MAT.Charts.DownloadCSVData'})} - - - +
+ {showMATButton && ( + + + + )} +
+ + {intl.formatMessage({ id: 'MAT.Charts.DownloadJSONData' })}{' '} + + + + {intl.formatMessage({ id: 'MAT.Charts.DownloadCSVData' })}{' '} + + +
+
) } -const Chart = React.memo(function Chart({testGroup = null, queryParams = {}, setState}) { +const Chart = 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,32 +80,35 @@ const Chart = React.memo(function Chart({testGroup = null, queryParams = {}, set return ( // - - - {(!chartData && !error) ? ( - - ) : ( - <> - - {!!chartData?.size && } - - )} - - {error && - -
- Error: {error.message} - - {JSON.stringify(error, null, 2)} - -
- }/> - } -
+
+ {!chartData && !error ? ( + + ) : ( + <> + + {!!chartData?.size && } + + )} + {error && ( + +
+ + Error: {error.message} + +
{JSON.stringify(error, null, 2)}
+
+ + } + /> + )} +
) }) diff --git a/components/CollapseTrigger.js b/components/CollapseTrigger.js deleted file mode 100644 index bf63d31f8..000000000 --- a/components/CollapseTrigger.js +++ /dev/null @@ -1,11 +0,0 @@ -import React from 'react' -import styled from 'styled-components' -import { MdExpandLess } from 'react-icons/md' - -export const CollapseTrigger = styled(MdExpandLess)` - cursor: pointer; - background-color: ${props => props.$bg || '#ffffff'}; - border-radius: 50%; - transform: ${props => props.$open ? 'rotate(0deg)': 'rotate(180deg)'}; - transition: transform 0.1s linear; -` diff --git a/components/CountryBox.js b/components/CountryBox.js index d4f16fafa..88b716497 100644 --- a/components/CountryBox.js +++ b/components/CountryBox.js @@ -1,32 +1,29 @@ import Flag from 'components/Flag' import { GridBox } from 'components/VirtualizedGrid' -import { Box, Flex, Text } from 'ooni-components' - -const CountryList = ({ countries, itemsPerRow = 6, gridGap = 3 }) => { - const gridTemplateColumns = ['1fr 1fr', '1fr 1fr', '1fr 1fr 1fr 1fr', [...Array(itemsPerRow)].map((i) => ('1fr')).join(' ')] +const CountryList = ({ countries, itemsPerRow = 6 }) => { return ( - + // lg:grid-cols-${itemsPerRow} is added to the safelist in tailwindConfig.config.js +
{countries.map((c) => ( - - {c.localisedName} - +
+
+ +
+
{c.localisedName}
+
} count={c.count} /> - )) - } - + ))} +
) } -export default CountryList \ No newline at end of file +export default CountryList diff --git a/components/DateRangePicker.js b/components/DateRangePicker.js index 45dba90dd..f4052b729 100644 --- a/components/DateRangePicker.js +++ b/components/DateRangePicker.js @@ -1,11 +1,8 @@ import { addDays, parse, sub } from 'date-fns' -import { Button } from 'ooni-components' -import React, { useMemo, useState } from 'react' +import { useMemo, useState } from 'react' import { DayPicker } from 'react-day-picker' -import 'react-day-picker/dist/style.css' import { useIntl } from 'react-intl' import OutsideClickHandler from 'react-outside-click-handler' -import styled from 'styled-components' import ar from 'date-fns/locale/ar' import de from 'date-fns/locale/de' @@ -24,58 +21,42 @@ import zhHant from 'date-fns/locale/zh-HK' import { getDirection } from 'components/withIntl' -const StyledDatetime = styled.div` -z-index: 99999; -position: absolute; -max-width: 300px; -background-color: #ffffff; -border: 1px solid ${props => props.theme.colors.gray2}; - -.rdp-cell { - padding: 2px 0; -} - -.rdp-day_selected:not([disabled]), -.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}; -} -` - -const StyledRangeButtons = styled.div` -margin: 1em 1em 0; -display: flex; -gap: 6px; -flex-wrap: wrap; -` - -const StyledFooter = styled.div` -display: flex; -justify-content: right; -gap: 6px; -` - const Footer = ({ handleRangeSelect, range, close }) => { const intl = useIntl() 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,46 +93,54 @@ 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 [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) } return ( - +
close()}> - {rangesList} - {rangesList}
+ toDate={tomorrow} selected={range} onSelect={onSelect} - footer={
} /> + classNames={{ + vhidden: 'sr-only', + caption: 'flex justify-center items-center h-10', + root: 'text-gray-800', + months: 'flex gap-4 relative px-4', + caption_label: 'text-xl px-1', + nav_button: + 'inline-flex justify-center items-center absolute top-0 w-10 h-10 rounded-full text-gray-600 hover:bg-gray-100', + nav_button_next: 'right-0', + nav_button_previous: 'left-0', + table: 'border-collapse border-spacing-0', + head_cell: 'w-10 h-10 uppercase align-middle text-center', + cell: 'w-10 h-10 align-middle text-center border-0 px-0', + day: 'rounded-full w-10 h-10 transition-colors hover:bg-blue-100 focus:outline-none focus-visible:ring focus-visible:ring-blue-300 focus-visible:ring-opacity-50 active:bg-blue-600 active:text-white', + day_selected: 'text-white bg-blue-500 hover:bg-blue-500', + day_today: 'font-bold', + day_disabled: + 'opacity-25 hover:bg-white active:bg-white active:text-gray-800', + day_outside: 'enabled:opacity-50', + day_range_middle: 'rounded-none', + day_range_end: 'rounded-l-none rounded-r-full', + day_range_start: 'rounded-r-none rounded-l-full', + day_hidden: 'hidden', + }} + footer={ +
+ } + /> - +
) } -export default DateRangePicker \ No newline at end of file +export default DateRangePicker diff --git a/components/DomainChart.js b/components/DomainChart.js index 2a6bd2b43..44c7a0674 100644 --- a/components/DomainChart.js +++ b/components/DomainChart.js @@ -1,11 +1,11 @@ -// TODO: refactor to use the same chart for circumvention tools and domains (no request on probe_cc change) -import GridChart, { prepareDataForGridChart } from 'components/aggregation/mat/GridChart' +import GridChart, { + prepareDataForGridChart, +} from 'components/aggregation/mat/GridChart' import { MATContextProvider } from 'components/aggregation/mat/MATContext' import { MATLink } from 'components/Chart' import { DetailsBox } from 'components/measurement/DetailsBox' import { useRouter } from 'next/router' -import { Box, Flex, Heading } from 'ooni-components' -import React, { useEffect, useMemo } from 'react' +import { memo, useEffect, useMemo } from 'react' import { FormattedMessage, useIntl } from 'react-intl' import { MATFetcher } from 'services/fetchers' import useSWR from 'swr' @@ -15,49 +15,52 @@ const swrOptions = { dedupingInterval: 10 * 60 * 1000, } -const Chart = React.memo(function Chart({testGroup = null, queryParams = {}, setState}) { - const {query: { probe_cc }} = useRouter() - const {locale} = useIntl() +const Chart = memo(function Chart({ queryParams = {}, setState }) { + const { + query: { probe_cc }, + } = useRouter() + const { locale } = useIntl() 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.sort((a, b) => (new Intl.Collator(locale).compare(a.probe_cc, b.probe_cc))) + let chartData = data.data.sort((a, b) => + new Intl.Collator(locale).compare(a.probe_cc, b.probe_cc), + ) - if (probe_cc) chartData = chartData.filter(d => probe_cc === d.probe_cc) + if (probe_cc) chartData = chartData.filter((d) => probe_cc === d.probe_cc) - const [reshapedData, rowKeys, rowLabels] = prepareDataForGridChart(chartData, queryParams, locale) + const [reshapedData, rowKeys, rowLabels] = prepareDataForGridChart( + chartData, + queryParams, + locale, + ) return [reshapedData, rowKeys, rowLabels] - }, [data, queryParams, probe_cc, locale]) - useEffect(()=> { + useEffect(() => { if (setState && data?.data) setState(data.data) }, [data, setState]) const headerOptions = { probe_cc: false, subtitle: false } - const linkParams = {...queryParams, ... probe_cc && {probe_cc}} + const linkParams = { ...queryParams, ...(probe_cc && { probe_cc }) } return ( // - - - {(!chartData && !error) ? ( +
+
+ {!chartData && !error ? ( ) : ( <> @@ -69,18 +72,23 @@ const Chart = React.memo(function Chart({testGroup = null, queryParams = {}, set {!!chartData?.size && } )} - - {error && - -
- Error: {error.message} - - {JSON.stringify(error, null, 2)} - -
- }/> - } - +
+ {error && ( + +
+ + Error: {error.message} + +
{JSON.stringify(error, null, 2)}
+
+ + } + /> + )} +
) }) diff --git a/components/Flag.js b/components/Flag.js index b6e18136d..b123929d7 100644 --- a/components/Flag.js +++ b/components/Flag.js @@ -1,45 +1,261 @@ -import React from 'react' +import Image from 'next/image' import PropTypes from 'prop-types' -import styled from 'styled-components' -var supportedCountryCodes = [ - 'ad', 'ae', 'af', 'ag', 'ai', 'al', 'am', 'ao', 'aq', 'ar', 'as', 'at', 'au', - 'aw', 'ax', 'az', 'ba', 'bb', 'bd', 'be', 'bf', 'bg', 'bh', 'bi', 'bj', 'bl', - 'bm', 'bn', 'bo', 'bq', 'br', 'bs', 'bt', 'bv', 'bw', 'by', 'bz', 'ca', 'cc', - 'cd', 'cf', 'cg', 'ch', 'ci', 'ck', 'cl', 'cm', 'cn', 'co', 'cr', 'cu', 'cv', - 'cw', 'cx', 'cy', 'cz', 'de', 'dj', 'dk', 'dm', 'do', 'dz', 'ec', 'ee', 'eg', - 'eh', 'er', 'es', 'et', 'eu', 'fi', 'fj', 'fk', 'fm', 'fo', 'fr', 'ga', 'gb', - 'gd', 'ge', 'gf', 'gg', 'gh', 'gi', 'gl', 'gm', 'gn', 'gp', 'gq', 'gr', 'gs', - 'gt', 'gu', 'gw', 'gy', 'hk', 'hm', 'hn', 'hr', 'ht', 'hu', 'id', 'ie', 'il', - 'im', 'in', 'io', 'iq', 'ir', 'is', 'it', 'je', 'jm', 'jo', 'jp', 'ke', 'kg', - 'kh', 'ki', 'km', 'kn', 'kp', 'kr', 'kw', 'ky', 'kz', 'la', 'lb', 'lc', 'li', - 'lk', 'lr', 'ls', 'lt', 'lu', 'lv', 'ly', 'ma', 'mc', 'md', 'me', 'mf', 'mg', - 'mh', 'mk', 'ml', 'mm', 'mn', 'mo', 'mp', 'mq', 'mr', 'ms', 'mt', 'mu', 'mv', - 'mw', 'mx', 'my', 'mz', 'na', 'nc', 'ne', 'nf', 'ng', 'ni', 'nl', 'no', 'np', - 'nr', 'nu', 'nz', 'om', 'pa', 'pe', 'pf', 'pg', 'ph', 'pk', 'pl', 'pm', 'pn', - 'pr', 'ps', 'pt', 'pw', 'py', 'qa', 're', 'ro', 'rs', 'ru', 'rw', 'sa', 'sb', - 'sc', 'sd', 'se', 'sg', 'sh', 'si', 'sj', 'sk', 'sl', 'sm', 'sn', 'so', 'sr', - 'ss', 'st', 'sv', 'sx', 'sy', 'sz', 'tc', 'td', 'tf', 'tg', 'th', 'tj', 'tk', - 'tl', 'tm', 'tn', 'to', 'tr', 'tt', 'tv', 'tw', 'tz', 'ua', 'ug', 'um', 'un', - 'us', 'uy', 'uz', 'va', 'vc', 've', 'vg', 'vi', 'vn', 'vu', 'wf', 'ws', 'ye', - 'yt', 'za', 'zm', 'zw'] +const supportedCountryCodes = [ + 'ad', + 'ae', + 'af', + 'ag', + 'ai', + 'al', + 'am', + 'ao', + 'aq', + 'ar', + 'as', + 'at', + 'au', + 'aw', + 'ax', + 'az', + 'ba', + 'bb', + 'bd', + 'be', + 'bf', + 'bg', + 'bh', + 'bi', + 'bj', + 'bl', + 'bm', + 'bn', + 'bo', + 'bq', + 'br', + 'bs', + 'bt', + 'bv', + 'bw', + 'by', + 'bz', + 'ca', + 'cc', + 'cd', + 'cf', + 'cg', + 'ch', + 'ci', + 'ck', + 'cl', + 'cm', + 'cn', + 'co', + 'cr', + 'cu', + 'cv', + 'cw', + 'cx', + 'cy', + 'cz', + 'de', + 'dj', + 'dk', + 'dm', + 'do', + 'dz', + 'ec', + 'ee', + 'eg', + 'eh', + 'er', + 'es', + 'et', + 'eu', + 'fi', + 'fj', + 'fk', + 'fm', + 'fo', + 'fr', + 'ga', + 'gb', + 'gd', + 'ge', + 'gf', + 'gg', + 'gh', + 'gi', + 'gl', + 'gm', + 'gn', + 'gp', + 'gq', + 'gr', + 'gs', + 'gt', + 'gu', + 'gw', + 'gy', + 'hk', + 'hm', + 'hn', + 'hr', + 'ht', + 'hu', + 'id', + 'ie', + 'il', + 'im', + 'in', + 'io', + 'iq', + 'ir', + 'is', + 'it', + 'je', + 'jm', + 'jo', + 'jp', + 'ke', + 'kg', + 'kh', + 'ki', + 'km', + 'kn', + 'kp', + 'kr', + 'kw', + 'ky', + 'kz', + 'la', + 'lb', + 'lc', + 'li', + 'lk', + 'lr', + 'ls', + 'lt', + 'lu', + 'lv', + 'ly', + 'ma', + 'mc', + 'md', + 'me', + 'mf', + 'mg', + 'mh', + 'mk', + 'ml', + 'mm', + 'mn', + 'mo', + 'mp', + 'mq', + 'mr', + 'ms', + 'mt', + 'mu', + 'mv', + 'mw', + 'mx', + 'my', + 'mz', + 'na', + 'nc', + 'ne', + 'nf', + 'ng', + 'ni', + 'nl', + 'no', + 'np', + 'nr', + 'nu', + 'nz', + 'om', + 'pa', + 'pe', + 'pf', + 'pg', + 'ph', + 'pk', + 'pl', + 'pm', + 'pn', + 'pr', + 'ps', + 'pt', + 'pw', + 'py', + 'qa', + 're', + 'ro', + 'rs', + 'ru', + 'rw', + 'sa', + 'sb', + 'sc', + 'sd', + 'se', + 'sg', + 'sh', + 'si', + 'sj', + 'sk', + 'sl', + 'sm', + 'sn', + 'so', + 'sr', + 'ss', + 'st', + 'sv', + 'sx', + 'sy', + 'sz', + 'tc', + 'td', + 'tf', + 'tg', + 'th', + 'tj', + 'tk', + 'tl', + 'tm', + 'tn', + 'to', + 'tr', + 'tt', + 'tv', + 'tw', + 'tz', + 'ua', + 'ug', + 'um', + 'un', + 'us', + 'uy', + 'uz', + 'va', + 'vc', + 've', + 'vg', + 'vi', + 'vn', + 'vu', + 'wf', + 'ws', + 'ye', + 'yt', + 'za', + 'zm', + 'zw', +] -const FlagImg = styled.img` - width: ${props => props.size}px; - height: ${props => props.size}px; - clip-path: circle(50% at 50% 50%); -` - -const FlagContainer = styled.div` - border-radius: 50%; - /* padding-left: 3px; */ - /* padding-top: 3px; */ - width: ${props => props.size + 2}px; - height: ${props => props.size + 2}px; - border: ${props => props.$border ? '1px solid white' : 'none'}; -` - -export const Flag = ({countryCode, size, border}) => { +export const Flag = ({ countryCode = 'zz', size = 60, border }) => { countryCode = countryCode.toLowerCase() if (supportedCountryCodes.indexOf(countryCode) === -1) { // Map unsupported country codes to ZZ @@ -47,20 +263,24 @@ export const Flag = ({countryCode, size, border}) => { } const src = `/static/flags/1x1/${countryCode}.svg` return ( - - - +
+ +
) } Flag.propTypes = { countryCode: PropTypes.string.isRequired, - size: PropTypes.number.isRequired -} - -Flag.defaultProps = { - countryCode: 'zz', - size: 60 + size: PropTypes.number.isRequired, } export default Flag diff --git a/components/Footer.js b/components/Footer.js index f5fdc93c0..ecc85193b 100755 --- a/components/Footer.js +++ b/components/Footer.js @@ -1,139 +1,137 @@ -import { Box, Container, Flex, Link } from 'ooni-components' import ExplorerLogo from 'ooni-components/svgs/logos/OONI-HorizontalMonochromeInverted.svg' import PropTypes from 'prop-types' -import React from 'react' import { useIntl } from 'react-intl' -import styled from 'styled-components' -const StyledFooter = styled.footer` - background-color: ${(props) => props.theme.colors.blue9}; - color: #ffffff; - font-size: 16px; - margin-top: 32px; -` - -const FooterBox = styled(Box)` - padding-top: 25px; - padding-bottom: 25px; -` - -const FooterHead = styled.div` - font-weight: bolder; - margin-bottom: 10px; -` - -const StyledFooterItem = styled(Link)` - && { - text-decoration: none; - color: #ffffff; - cursor: pointer; - opacity: 0.5; - display: ${(props) => (props.$horizontal === 'true' ? 'inline' : 'block')}; - margin-left: ${(props) => (props.$horizontal === 'true' ? '1rem' : 0)}; - &:hover { - opacity: 1; - } - } -` +const FooterHead = ({ ...props }) => ( +
+) -const FooterLink = ({ label, href, horizontal = false }) => ( - // Use non-boolean value for props sent to non-DOM styled components - // https://www.styled-components.com/docs/faqs#why-am-i-getting-html-attribute-warnings - +const FooterLink = ({ label, ...props }) => ( + {label} - + ) FooterLink.propTypes = { label: PropTypes.node, href: PropTypes.string.isRequired, - horizontal: PropTypes.bool, } -const FooterText = styled.div` - margin-top: 0px; -` - const Footer = () => { const intl = useIntl() const currentYear = new Intl.DateTimeFormat(intl.locale, { year: 'numeric', }).format(new Date()) return ( - - - - - - - - - +
+
+
+
+
+
+ +
+
{' '} - {intl.formatMessage({ id: 'Footer.Text.Slogan' })}{' '} - - - - - {intl.formatMessage({ id: 'Footer.Heading.About' })} - +
+ {intl.formatMessage({ id: 'Footer.Text.Slogan' })} +
{' '} +
+
+
+
+ + {intl.formatMessage({ id: 'Footer.Heading.About' })} + + - - - {intl.formatMessage({ id: 'Footer.Heading.OONIProbe' })} - - - - - - - {intl.formatMessage({ id: 'Footer.Heading.Updates' })} - +
+
+ + {intl.formatMessage({ id: 'Footer.Heading.OONIProbe' })} + + + + + +
+
+ + {intl.formatMessage({ id: 'Footer.Heading.Updates' })} + + - - - - +
+
+
+
- {intl.formatMessage({ id: 'Footer.Text.Copyright' }, { currentYear })} +
+ {intl.formatMessage( + { id: 'Footer.Text.Copyright' }, + { currentYear }, + )} +
- - {intl.formatMessage({ id: 'Footer.Text.Version' })}: {process.env.GIT_COMMIT_SHA_SHORT} - +
+ {intl.formatMessage({ id: 'Footer.Text.Version' })}:{' '} + {process.env.GIT_COMMIT_SHA_SHORT} +
- - - - +
+
+
+
) } diff --git a/components/FormattedMarkdown.js b/components/FormattedMarkdown.js index 7c1a814dc..cfffc3fbd 100644 --- a/components/FormattedMarkdown.js +++ b/components/FormattedMarkdown.js @@ -1,23 +1,37 @@ // Documentation for markdown-to-jsx // https://github.com/probablyup/markdown-to-jsx -import React from 'react' +import Markdown from 'markdown-to-jsx' import PropTypes from 'prop-types' import { useIntl } from 'react-intl' -import Markdown from 'markdown-to-jsx' -import { Link, theme } from 'ooni-components' +import { twMerge } from 'tailwind-merge' + +const MdH1 = ({ children, className, ...props }) => ( +

+ {children} +

+) + +const MdUL = ({ children, className, ...props }) => ( +
    + {children} +
+) + +const MdP = ({ children, className, ...props }) => ( +

+ {children} +

+) export const FormattedMarkdownBase = ({ children }) => { return ( {children} @@ -30,7 +44,7 @@ const FormattedMarkdown = ({ id, defaultMessage, values }) => { return ( - {intl.formatMessage({id, defaultMessage}, values )} + {intl.formatMessage({ id, defaultMessage }, values)} ) } @@ -38,7 +52,7 @@ const FormattedMarkdown = ({ id, defaultMessage, values }) => { FormattedMarkdown.propTypes = { id: PropTypes.string.isRequired, defaultMessage: PropTypes.string, - values: PropTypes.object + values: PropTypes.object, } export default FormattedMarkdown diff --git a/components/GridLoader.js b/components/GridLoader.js index 8213a48bb..ce63862f0 100644 --- a/components/GridLoader.js +++ b/components/GridLoader.js @@ -1,8 +1,7 @@ -import React from 'react' import ContentLoader from 'react-content-loader' const Loader = (props) => ( - ( foregroundColor="#ecebeb" {...props} > - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ) diff --git a/components/Header.js b/components/Header.js index 89c550653..0c643dc44 100644 --- a/components/Header.js +++ b/components/Header.js @@ -1,4 +1,3 @@ -import React from 'react' import Head from 'next/head' import { useRouter } from 'next/router' import { useIntl } from 'react-intl' @@ -7,7 +6,7 @@ const Header = () => { const { asPath, locale, defaultLocale } = useRouter() const lang = locale === defaultLocale ? '' : `/${locale}` - const canonical = 'https://explorer.ooni.org' + lang + asPath.split('?')[0] + const canonical = `https://explorer.ooni.org${lang}${asPath.split('?')[0]}` const intl = useIntl() const description = intl.formatMessage({ id: 'Home.Meta.Description' }) @@ -15,15 +14,44 @@ const Header = () => { return ( - - - - - - - - - + + + + + + + + + @@ -34,10 +62,14 @@ const Header = () => { - + ) } -export default Header \ No newline at end of file +export default Header diff --git a/components/Layout.js b/components/Layout.js index 2ff31f116..f3436c507 100644 --- a/components/Layout.js +++ b/components/Layout.js @@ -1,10 +1,8 @@ -import { Box, theme } from 'ooni-components' import PropTypes from 'prop-types' -import React, { useMemo } from 'react' -import { ThemeProvider, createGlobalStyle } from 'styled-components' +import { useMemo } from 'react' -import { StyledStickyNavBar, StyledStickySubMenu } from 'components/SharedStyledComponents' -import { getDirection } from 'components/withIntl' +import { StyledStickyNavBar } from 'components/SharedStyledComponents' +// import { getDirection } from 'components/withIntl' import { UserProvider } from 'hooks/useUser' import { useRouter } from 'next/router' import { useIntl } from 'react-intl' @@ -12,83 +10,50 @@ import Footer from './Footer' import Header from './Header' import NavBar from './NavBar' -theme.maxWidth = 1024 - -const GlobalStyle = createGlobalStyle` - * { - text-rendering: geometricPrecision; - box-sizing: border-box; - } - body, html { - // direction: ${props => props.direction}; - margin: 0; - padding: 0; - font-size: 14px; - height: 100%; - background-color: #ffffff; - } - a { - text-decoration: none; - color: ${(props) => props.theme.colors.blue6}; - &:hover { - color: ${(props) => props.theme.colors.blue9}; - } - } - /* - Sticky Footer fix - Based on: https://philipwalton.github.io/solved-by-flexbox/demos/sticky-footer/ - */ - .site { - display: flex; - flex-direction: column; - min-height: 100vh; - } - .content { - flex: 1 0 auto; - } -` - const Layout = ({ children }) => { const { locale } = useIntl() const { pathname } = useRouter() const navbarColor = useMemo(() => { - return pathname === '/' || pathname.match(/^\/m\/\S+/) || pathname.match(/^\/measurement\/\S+/) ? - 'transparent' - : null + return pathname === '/' || + pathname.match(/^\/m\/\S+/) || + pathname.match(/^\/measurement\/\S+/) + ? null + : 'bg-blue-500' }, [pathname]) + const navbarSticky = useMemo(() => { - return pathname === '/countries' || - pathname === '/domains' || - pathname === '/networks' || - pathname === '/findings' || - pathname.match(/^\/country\/\S+/) + return ( + pathname === '/countries' || + pathname === '/domains' || + pathname === '/networks' || + pathname === '/findings' || + pathname.match(/^\/country\/\S+/) + ) }, [pathname]) return ( - - - -
-
- {navbarSticky ? - - - : + +
+
+ {navbarSticky ? ( + - } - - { children } - -
+ + ) : ( + + )} +
+ {children}
- - +
+
+
) } Layout.propTypes = { - children: PropTypes.object.isRequired + children: PropTypes.object.isRequired, } export default Layout diff --git a/components/MATChart.js b/components/MATChart.js index e2e25269d..1482c5af7 100644 --- a/components/MATChart.js +++ b/components/MATChart.js @@ -1,15 +1,14 @@ -import { Box, Text } from 'ooni-components' -import { StackedBarChart } from 'components/aggregation/mat/StackedBarChart' +import axios from 'axios' import { FunnelChart } from 'components/aggregation/mat/FunnelChart' +import { MATContextProvider } from 'components/aggregation/mat/MATContext' import { NoCharts } from 'components/aggregation/mat/NoCharts' +import { StackedBarChart } from 'components/aggregation/mat/StackedBarChart' import TableView from 'components/aggregation/mat/TableView' -import { useMemo } from 'react' -import useSWR from 'swr' -import dayjs from 'services/dayjs' import { axiosResponseTime } from 'components/axios-plugins' -import axios from 'axios' -import { MATContextProvider } from 'components/aggregation/mat/MATContext' +import { useMemo } from 'react' import { useIntl } from 'react-intl' +import dayjs from 'services/dayjs' +import useSWR from 'swr' import { FormattedMarkdownBase } from './FormattedMarkdown' axiosResponseTime(axios) @@ -65,47 +64,62 @@ export const MATChartWrapper = ({ link, caption }) => { ...searchParams, } - return !!searchParams && - - - {caption && ( - - {captionText} - - )} - + return ( + !!searchParams && ( +
+ + {caption && ( +
+ {captionText} +
+ )} +
+ ) + ) } const MATChart = ({ query, showFilters = true }) => { const intl = useIntl() - const { data, error, isValidating } = useSWR(query ? query : null, fetcher, swrOptions) + const { data, error, isValidating } = useSWR( + query ? query : null, + fetcher, + swrOptions, + ) const showLoadingIndicator = useMemo(() => isValidating, [isValidating]) return ( - + <> {error && } - + <> {showLoadingIndicator ? ( - -

{intl.formatMessage({ id: 'General.Loading' })}

-
+

{intl.formatMessage({ id: 'General.Loading' })}

) : ( <> - {data?.data?.result?.length > 0 ? - - {data && data.data.dimension_count == 0 && } - {data && data.data.dimension_count == 1 && } + {data?.data?.result?.length > 0 ? ( + <> + {data && data.data.dimension_count === 0 && ( + + )} + {data && data.data.dimension_count === 1 && ( + + )} {data && data.data.dimension_count > 1 && ( - + )} - : - } + + ) : ( + + )} )} -
+
-
+ ) } diff --git a/components/NavBar.js b/components/NavBar.js index d3e4e20b5..752c74041 100644 --- a/components/NavBar.js +++ b/components/NavBar.js @@ -1,129 +1,58 @@ import useUser from 'hooks/useUser' -import NLink from 'next/link' +import Link from 'next/link' import { useRouter } from 'next/router' -import { Box, Container, Flex } from 'ooni-components' import ExplorerLogo from 'ooni-components/svgs/logos/Explorer-HorizontalMonochromeInverted.svg' -import React, { useEffect, useState } from 'react' +import { useEffect, useState } from 'react' import { MdClose, MdMenu } from 'react-icons/md' import { FormattedMessage, useIntl } from 'react-intl' -import styled from 'styled-components' import { getLocalisedLanguageName } from 'utils/i18nCountries' import { getDirection } from './withIntl' -const StyledNavItem = styled(NLink)` - position: relative; - color: ${(props) => props.theme.colors.white}; - cursor: pointer; - padding-bottom: ${(props) => (props.$active ? '4px' : '6px')}; - opacity: ${(props) => (props.$active ? '1' : '0.6')}; - border-bottom: ${(props) => (props.$active ? `2px solid ${props.theme.colors.white}` : 'none')}; - - &:hover { - padding-bottom: 4px; - color: ${(props) => props.theme.colors.white}; - opacity: 1; - border-bottom: 2px solid ${(props) => props.theme.colors.white}; - } -` - -const LanguageSelect = styled.select` -color: ${(props) => props.theme.colors.white}; -background: none; -opacity: 0.6; -border: none; -text-transform: capitalize; -cursor: pointer; -font-family: inherit; -font-size: inherit; -padding: 0; -padding-bottom: 6px; -outline: none; -appearance: none; --webkit-appearance: none; --moz-appearance: none; --ms-appearance: none; --o-appearance: none; -&:hover { - opacity: 1; -} -// reset option styling for browsers that apply it to its native styling (Brave) -> option { - color: initial; - opacity: initial; -} -` +const LanguageSelect = () => ( + - - ) - } -) + return ( + <> + + + ) +}) IndeterminateCheckbox.displayName = 'IndeterminateCheckbox' const SearchFilter = ({ @@ -81,23 +47,17 @@ const SearchFilter = ({ return ( { + onChange={(e) => { setFilter(e.target.value || undefined) // Set undefined to remove the filter entirely }} - placeholder={intl.formatMessage({id: 'MAT.Table.FilterPlaceholder'}, {count})} + placeholder={intl.formatMessage( + { id: 'MAT.Table.FilterPlaceholder' }, + { count }, + )} /> ) } -const StyledGlobalFilter = styled(Box)` - margin: 16px; - margin-top: 10px; - input { - border: 0; - outline: 0; - } -` - function GlobalFilter({ preGlobalFilteredRows, globalFilter, @@ -106,7 +66,7 @@ function GlobalFilter({ const intl = useIntl() const count = preGlobalFilteredRows.length const [value, setValue] = useState(globalFilter) - const onChange = useAsyncDebounce(value => { + const onChange = useAsyncDebounce((value) => { setGlobalFilter(value || '') }, 200) @@ -117,28 +77,27 @@ function GlobalFilter({ }, [globalFilter]) return ( - - {intl.formatMessage({id: 'MAT.Table.Search'})}{' '} +
+ {intl.formatMessage({ id: 'MAT.Table.Search' })}{' '} { + onChange={(e) => { setValue(e.target.value) onChange(e.target.value) }} - placeholder={intl.formatMessage({id: 'MAT.Table.FilterPlaceholder'}, {count})} + placeholder={intl.formatMessage( + { id: 'MAT.Table.FilterPlaceholder' }, + { count }, + )} /> - +
) } const SortHandle = ({ isSorted, isSortedDesc }) => { return ( - - {isSorted ? ( - isSortedDesc ? '▼' : '▲' - ) : ( -   - )} + <>{isSorted ? isSortedDesc ? : : } ) } @@ -158,81 +117,87 @@ const Filters = ({ data = [], tableData, setDataForCharts, query }) => { Filter: SearchFilter, Cell: ({ value }) => { const intl = useIntl() - return typeof value === 'number' ? intl.formatNumber(value, {}) : String(value) - } + return typeof value === 'number' + ? intl.formatNumber(value, {}) + : String(value) + }, }), - [] + [], ) // Aggregate by the first column - const initialState = useMemo(() => ({ - hiddenColumns: ['yAxisCode'], - sortBy: [{ id: 'yAxisLabel', desc: false }] - }),[]) + const initialState = useMemo( + () => ({ + hiddenColumns: ['yAxisCode'], + sortBy: [{ id: 'yAxisLabel', desc: false }], + }), + [], + ) - const getRowId = useCallback(row => row[query.axis_y], [query.axis_y]) + const getRowId = useCallback((row) => row[query.axis_y], [query.axis_y]) - const columns = useMemo(() => [ - { - Header: intl.formatMessage({ id: `MAT.Table.Header.${yAxis}`}), - Cell: ({ value, row }) => ( - - {value} - - ), - id: 'yAxisLabel', - accessor: 'rowLabel', - filter: 'text', - style: { - width: '35%' - } - }, - { - id: 'yAxisCode', - accessor: yAxis, - disableFilters: true, - }, - { - Header: , - accessor: 'anomaly_count', - width: 150, - sortDescFirst: true, - disableFilters: true, - style: { - textAlign: 'end' - } - }, - { - Header: , - accessor: 'confirmed_count', - width: 150, - sortDescFirst: true, - disableFilters: true, - style: { - textAlign: 'end' - } - }, - { - Header: , - accessor: 'failure_count', - width: 150, - sortDescFirst: true, - disableFilters: true, - style: { - textAlign: 'end' - } - }, - { - Header: , - accessor: 'measurement_count', - width: 150, - sortDescFirst: true, - disableFilters: true, - style: { - textAlign: 'end' - } - } - ], [intl, yAxis]) + const columns = useMemo( + () => [ + { + Header: intl.formatMessage({ id: `MAT.Table.Header.${yAxis}` }), + Cell: ({ value, row }) => ( +
{value}
+ ), + id: 'yAxisLabel', + accessor: 'rowLabel', + filter: 'text', + style: { + width: '35%', + }, + }, + { + id: 'yAxisCode', + accessor: yAxis, + disableFilters: true, + }, + { + Header: , + accessor: 'anomaly_count', + width: 150, + sortDescFirst: true, + disableFilters: true, + style: { + textAlign: 'end', + }, + }, + { + Header: , + accessor: 'confirmed_count', + width: 150, + sortDescFirst: true, + disableFilters: true, + style: { + textAlign: 'end', + }, + }, + { + Header: , + accessor: 'failure_count', + width: 150, + sortDescFirst: true, + disableFilters: true, + style: { + textAlign: 'end', + }, + }, + { + Header: , + accessor: 'measurement_count', + width: 150, + sortDescFirst: true, + disableFilters: true, + style: { + textAlign: 'end', + }, + }, + ], + [intl, yAxis], + ) const { getTableProps, @@ -279,22 +244,33 @@ const Filters = ({ data = [], tableData, setDataForCharts, query }) => {
- ) + ), }, - ...columns + ...columns, ]) - } + }, ) - + const updateCharts = useCallback(() => { - const selectedRows = Object.keys(state.selectedRowIds).sort((a,b) => sortRows(a, b, query.axis_y, intl.locale)) + const selectedRows = Object.keys(state.selectedRowIds).sort((a, b) => + sortRows(a, b, query.axis_y, intl.locale), + ) - if (selectedRows.length > 0 && selectedRows.length !== preGlobalFilteredRows.length) { + if ( + selectedRows.length > 0 && + selectedRows.length !== preGlobalFilteredRows.length + ) { setDataForCharts(selectedRows) } else { setDataForCharts(noRowsSelected) } - }, [preGlobalFilteredRows.length, query.axis_y, state.selectedRowIds, setDataForCharts, intl.locale]) + }, [ + preGlobalFilteredRows.length, + query.axis_y, + state.selectedRowIds, + setDataForCharts, + intl.locale, + ]) /** * Reset the table filter @@ -312,10 +288,15 @@ const Filters = ({ data = [], tableData, setDataForCharts, query }) => { setGlobalFilter('') } setDataForCharts(noRowsSelected) - }, [setGlobalFilter, state.globalFilter, toggleAllRowsSelected, setDataForCharts]) + }, [ + setGlobalFilter, + state.globalFilter, + toggleAllRowsSelected, + setDataForCharts, + ]) useEffect(() => { - if (state.globalFilter == undefined && resetTableRef.current === true) { + if (state.globalFilter === undefined && resetTableRef.current === true) { resetTableRef.current = false toggleAllRowsSelected(false) } @@ -331,64 +312,91 @@ const Filters = ({ data = [], tableData, setDataForCharts, query }) => { }) return ( - - - - - - - - - + +
+
+ + +
+
+
+
{headerGroups.map((headerGroup, i) => ( - +
{headerGroup.headers.map((column, i) => { return ( - - +
+ {column.render('Header')} - {column.canSort && - - } + {column.canSort && ( + + )} - - )} - )} - +
+ ) + })} +
))} - +
- - +
+
- - {virtualRows.map(virtualRow => { + {virtualRows.map((virtualRow) => { const row = rows[virtualRow.index] prepareRow(row) return ( - { left: 0, width: '100%', height: `${virtualRow.size}px`, - transform: `translateY(${virtualRow.start}px)` - } - })}> + transform: `translateY(${virtualRow.start}px)`, + }, + })} + > {row.cells.map((cell, i) => { return ( - + style: cell.column.style, + }, + ])} + > {cell.render('Cell')} - +
) })} - +
) })} - +
-
-
-
+
+
+ ) } -export default Filters \ No newline at end of file +export default Filters diff --git a/components/aggregation/mat/Form.js b/components/aggregation/mat/Form.js index 7bd5e4c9a..3db7dc275 100644 --- a/components/aggregation/mat/Form.js +++ b/components/aggregation/mat/Form.js @@ -1,13 +1,12 @@ import { format } from 'date-fns' -import { Box, Button, Flex, Input, Select } from 'ooni-components' +import { useRouter } from 'next/router' +import { Input, Select } from 'ooni-components' import PropTypes from 'prop-types' import { useCallback, useEffect, useMemo, useState } from 'react' import { Controller, useForm } from 'react-hook-form' import { FormattedMessage, defineMessages, useIntl } from 'react-intl' import dayjs from 'services/dayjs' import { localisedCountries } from 'utils/i18nCountries' - -import { useRouter } from 'next/router' import DateRangePicker from '../../DateRangePicker' import { TestNameOptions } from '../../TestNameOptions' import { categoryCodes } from '../../utils/categoryCodes' @@ -73,13 +72,24 @@ const yAxisOptions = [ ['', [], false], ] -const testsWithValidDomainFilter = ['web_connectivity', 'http_requests', 'dns_consistency', 'tcp_connect'] +const testsWithValidDomainFilter = [ + 'web_connectivity', + 'http_requests', + 'dns_consistency', + 'tcp_connect', +] -const filterAxisOptions = (options, countryValue = '', testNameValue = 'web_connectivity') => { +const filterAxisOptions = ( + options, + countryValue = '', + testNameValue = 'web_connectivity', +) => { return options .filter(([option, validTestNames, hideForSingleCountry]) => { if (hideForSingleCountry && countryValue !== '') return false - return validTestNames.length === 0 || validTestNames.includes(testNameValue) + return ( + validTestNames.length === 0 || validTestNames.includes(testNameValue) + ) }) .map(([option]) => option) } @@ -111,7 +121,10 @@ export const Form = ({ onSubmit, query }) => { const router = useRouter() const [showConfirmation, setShowConfirmation] = useState(false) - const defaultValues = useMemo(() => Object.assign({}, defaultDefaultValues, query), [query]) + const defaultValues = useMemo( + () => Object.assign({}, defaultDefaultValues, query), + [query], + ) const { handleSubmit, control, getValues, watch, reset, setValue } = useForm({ defaultValues, @@ -124,31 +137,35 @@ export const Form = ({ onSubmit, query }) => { } }, [defaultValues, reset, router.isReady]) - const [since, setSince] = useState(defaultValues['since']) - const [until, setUntil] = useState(defaultValues['until']) - const [countryValue, setCountryValue] = useState(defaultValues['probe_cc']) - const [testNameValue, setTestNameValue] = useState(defaultValues['test_name']) + const [since, setSince] = useState(defaultValues.since) + const [until, setUntil] = useState(defaultValues.until) + const [countryValue, setCountryValue] = useState(defaultValues.probe_cc) + const [testNameValue, setTestNameValue] = useState(defaultValues.test_name) useEffect(() => { - const subscription = watch((value, { name, type }) => { - if (name === 'since') setSince(value['since']) - if (name === 'until') setUntil(value['until']) - if (name === 'probe_cc') setCountryValue(value['probe_cc']) - if (name === 'test_name') setTestNameValue(value['test_name']) + const subscription = watch((value, { name }) => { + if (name === 'since') setSince(value.since) + if (name === 'until') setUntil(value.until) + if (name === 'probe_cc') setCountryValue(value.probe_cc) + if (name === 'test_name') setTestNameValue(value.test_name) }) return () => subscription.unsubscribe() }, [watch]) - const sortedCountries = useMemo(() => ( + const sortedCountries = useMemo( + () => localisedCountries(intl.locale).sort((a, b) => - new Intl.Collator(intl.locale).compare(a.localisedCountryName, b.localisedCountryName)) - ), - [intl.locale] + new Intl.Collator(intl.locale).compare( + a.localisedCountryName, + b.localisedCountryName, + ), + ), + [intl.locale], ) const showWebConnectivityFilters = useMemo( () => isValidFilterForTestname(testNameValue, testsWithValidDomainFilter), - [testNameValue] + [testNameValue], ) // reset domain and input when web_connectivity is deselected useEffect(() => { @@ -178,7 +195,7 @@ export const Form = ({ onSubmit, query }) => { setShowConfirmation(false) handleSubmit(onSubmit)(e) }, - [handleSubmit, onSubmit] + [handleSubmit, onSubmit], ) const onCancel = useCallback((e) => { @@ -190,7 +207,11 @@ export const Form = ({ onSubmit, query }) => { (e) => { e.preventDefault() - const [since, until, timeGrain] = getValues(['since', 'until', 'time_grain']) + const [since, until, timeGrain] = getValues([ + 'since', + 'until', + 'time_grain', + ]) const shouldShowConfirmationModal = () => { if (timeGrain === 'month') return false const diff = dayjs(until).diff(dayjs(since), 'month') @@ -205,7 +226,7 @@ export const Form = ({ onSubmit, query }) => { onConfirm(e) } }, - [getValues, onConfirm] + [getValues, onConfirm], ) const xAxisOptionsFiltered = useMemo(() => { @@ -213,7 +234,8 @@ export const Form = ({ onSubmit, query }) => { }, [testNameValue, countryValue]) useEffect(() => { - if (!xAxisOptionsFiltered.includes(getValues('axis_x'))) setValue('axis_x', 'measurement_start_day') + if (!xAxisOptionsFiltered.includes(getValues('axis_x'))) + setValue('axis_x', 'measurement_start_day') }, [setValue, getValues, xAxisOptionsFiltered]) const yAxisOptionsFiltered = useMemo(() => { @@ -221,37 +243,54 @@ export const Form = ({ onSubmit, query }) => { }, [testNameValue, countryValue]) useEffect(() => { - if (!yAxisOptionsFiltered.includes(getValues('axis_y'))) setValue('axis_y', '') + if (!yAxisOptionsFiltered.includes(getValues('axis_y'))) + setValue('axis_y', '') }, [setValue, getValues, yAxisOptionsFiltered]) const timeGrainOptions = useMemo(() => { const dateRegex = /^\d{4}-\d{2}-\d{2}$/ - if (!until?.match(dateRegex) || !since?.match(dateRegex)) return ['hour', 'day', 'week', 'month'] + if (!until?.match(dateRegex) || !since?.match(dateRegex)) + return ['hour', 'day', 'week', 'month'] const diff = dayjs(until).diff(dayjs(since), 'day') if (diff < 8) { const availableValues = ['hour', 'day'] - if (!availableValues.includes(getValues('time_grain'))) setValue('time_grain', 'hour') + if (!availableValues.includes(getValues('time_grain'))) + setValue('time_grain', 'hour') return availableValues - } else if (diff >= 8 && diff < 31) { + } + if (diff >= 8 && diff < 31) { const availableValues = ['day', 'week'] - if (!availableValues.includes(getValues('time_grain'))) setValue('time_grain', 'day') + if (!availableValues.includes(getValues('time_grain'))) + setValue('time_grain', 'day') return availableValues - } else if (diff >= 31) { + } + if (diff >= 31) { const availableValues = ['day', 'week', 'month'] - if (!availableValues.includes(getValues('time_grain'))) setValue('time_grain', 'day') + if (!availableValues.includes(getValues('time_grain'))) + setValue('time_grain', 'day') return availableValues } }, [setValue, getValues, since, until]) return (
- - - + +
+
( - + {sortedCountries.map((c, idx) => (
+
( - + )} /> - - - - - ( - setShowDatePicker(true)} - onKeyDown={() => setShowDatePicker(false)} - /> - )} - /> - - - ( - setShowDatePicker(true)} - onKeyDown={() => setShowDatePicker(false)} - /> - )} - /> - - +
+
+
+ ( + setShowDatePicker(true)} + onKeyDown={() => setShowDatePicker(false)} + /> + )} + /> + ( + setShowDatePicker(true)} + onKeyDown={() => setShowDatePicker(false)} + /> + )} + /> +
{showDatePicker && ( { close={() => setShowDatePicker(false)} /> )} - - +
+
( - {timeGrainOptions.map((option, idx) => (
+
( - {xAxisOptionsFiltered.map((option, idx) => ( ))} )} /> - - +
+
( - {yAxisOptionsFiltered.map((option, idx) => ( ))} )} /> - - - - +
+
+
+
( - )} /> - +
{showWebConnectivityFilters && ( <> - +
( )} /> - - +
+
( )} /> - - +
+
( )} /> - +
)} - - - - +
+
+ +
) } diff --git a/components/aggregation/mat/FunnelChart.js b/components/aggregation/mat/FunnelChart.js index 857c75a1c..dde60f509 100644 --- a/components/aggregation/mat/FunnelChart.js +++ b/components/aggregation/mat/FunnelChart.js @@ -1,20 +1,21 @@ -import React from 'react' import { ResponsiveFunnel } from '@nivo/funnel' -import { theme } from 'ooni-components' +import { colors } from 'ooni-components' const stateColors = { - 'ok': theme.colors.green8, - 'failure': theme.colors.gray6, - 'anomaly': theme.colors.yellow9, - 'confirmed': theme.colors.red7, + ok: colors.green['800'], + failure: colors.gray['600'], + anomaly: colors.yellow['900'], + confirmed: colors.red['700'], } const reshapeData = (data) => { - return Object.entries(data).map((entry) => ({ - 'id': entry[0], - 'value': entry[1], - 'label': `${entry[0].split('_')[0]}` - })).sort((a,b) => a.value < b.value) + return Object.entries(data) + .map((entry) => ({ + id: entry[0], + value: entry[1], + label: `${entry[0].split('_')[0]}`, + })) + .sort((a, b) => a.value < b.value) } export const FunnelChart = ({ data }) => ( @@ -22,9 +23,9 @@ export const FunnelChart = ({ data }) => ( data={reshapeData(data)} margin={{ top: 20, right: 20, bottom: 20, left: 20 }} valueFormat=">-.2s" - colors={d => stateColors[d.id.split('_')[0]] ?? theme.colors.blue5} + colors={(d) => stateColors[d.id.split('_')[0]] ?? colors.blue['500']} borderWidth={20} - labelColor={{ from: 'color', modifiers: [ [ 'darker', 3 ] ] }} + labelColor={{ from: 'color', modifiers: [['darker', 3]] }} beforeSeparatorLength={100} beforeSeparatorOffset={20} afterSeparatorLength={100} @@ -33,4 +34,4 @@ export const FunnelChart = ({ data }) => ( currentBorderWidth={40} motionConfig="wobbly" /> -) \ No newline at end of file +) diff --git a/components/aggregation/mat/GridChart.js b/components/aggregation/mat/GridChart.js index 944c0aa19..543fc6b9d 100644 --- a/components/aggregation/mat/GridChart.js +++ b/components/aggregation/mat/GridChart.js @@ -1,18 +1,16 @@ -import React, { useMemo, useRef } from 'react' -import PropTypes from 'prop-types' -import { TooltipProvider, Tooltip } from '@nivo/tooltip' import { Container } from '@nivo/core' -import { Flex } from 'ooni-components' - -import RowChart from './RowChart' -import { sortRows, fillDataHoles } from './computations' -import { useMATContext } from './MATContext' +import { Tooltip, TooltipProvider } from '@nivo/tooltip' +import PropTypes from 'prop-types' +import { memo, useMemo, useRef } from 'react' import { ChartHeader } from './ChartHeader' -import { getRowLabel } from './labels' -import { VirtualRows } from './VirtualRows' -import { XAxis } from './XAxis' import { barThemeForTooltip } from './CustomTooltip' +import { useMATContext } from './MATContext' import { NoCharts } from './NoCharts' +import RowChart from './RowChart' +import { VirtualRows } from './VirtualRows' +import { XAxis } from './XAxis' +import { fillDataHoles, sortRows } from './computations' +import { getRowLabel } from './labels' const ROW_HEIGHT = 70 const XAXIS_HEIGHT = 62 @@ -60,7 +58,9 @@ export const prepareDataForGridChart = (data, query, locale) => { const reshapedDataWithoutHoles = fillDataHoles(reshapedData, query) - const sortedRowKeys = rows.sort((a, b) => (sortRows(rowLabels[a], rowLabels[b], query.axis_y, locale))) + const sortedRowKeys = rows.sort((a, b) => + sortRows(rowLabels[a], rowLabels[b], query.axis_y, locale), + ) return [reshapedDataWithoutHoles, sortedRowKeys, rowLabels] } @@ -86,11 +86,18 @@ export const prepareDataForGridChart = (data, query, locale) => { * header - an element showing some summary information on top of the charts } */ -const GridChart = ({ data, rowKeys, rowLabels, height = 'auto', header, selectedRows = null, noLabels = false }) => { - +const GridChart = ({ + data, + rowKeys, + rowLabels, + height = 'auto', + header, + selectedRows = null, + noLabels = false, +}) => { // Fetch query state from context instead of router // because some params not present in the URL are injected in the context - const [ query ] = useMATContext() + const [query] = useMATContext() const { tooltipIndex } = query const indexBy = query.axis_x const tooltipContainer = useRef(null) @@ -108,39 +115,38 @@ const GridChart = ({ data, rowKeys, rowLabels, height = 'auto', header, selected let gridHeight = height if (height === 'auto') { const rowCount = selectedRows?.length ?? rowKeys.length - gridHeight = Math.min( XAXIS_HEIGHT + (rowCount * ROW_HEIGHT), GRID_MAX_HEIGHT) + gridHeight = Math.min(XAXIS_HEIGHT + rowCount * ROW_HEIGHT, GRID_MAX_HEIGHT) } if (!data || data.size < 1) { - return ( - - ) + return } // To correctly align with the rows, generate a data row with only x-axis values // e.g [ {measurement_start_day: '2022-01-01'}, {measurement_start_day: '2022-01-02'}... ] - const xAxisData = data.get(rowKeys[0]).map(d => ({ [query.axis_x]: d[query.axis_x]})) + const xAxisData = data + .get(rowKeys[0]) + .map((d) => ({ [query.axis_x]: d[query.axis_x] })) const rowHeight = noLabels ? 500 : ROW_HEIGHT return ( - - +
+
{/* Fake axis on top of list. Possible alternative: dummy chart with axis and valid tickValues */} {/* Use a virtual list only for higher count of rows */} {rowsToRender.length < 10 ? ( - {!noLabels && } - {rowsToRender.map((rowKey, index) => + {rowsToRender.map((rowKey, index) => ( - )} - + ))} +
) : ( } @@ -162,8 +168,8 @@ const GridChart = ({ data, rowKeys, rowLabels, height = 'auto', header, selected tooltipIndex={tooltipIndex} /> )} - - +
+
@@ -175,12 +181,9 @@ GridChart.propTypes = { rowKeys: PropTypes.arrayOf(PropTypes.string), rowLabels: PropTypes.objectOf(PropTypes.string), selectedRows: PropTypes.arrayOf(PropTypes.string), - height: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.number - ]), + height: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), header: PropTypes.element, - noLabels: PropTypes.bool + noLabels: PropTypes.bool, } -export default React.memo(GridChart) +export default memo(GridChart) diff --git a/components/aggregation/mat/HeatMapChart.js b/components/aggregation/mat/HeatMapChart.js deleted file mode 100644 index 78def1db7..000000000 --- a/components/aggregation/mat/HeatMapChart.js +++ /dev/null @@ -1,216 +0,0 @@ -import React from 'react' -import PropTypes from 'prop-types' -import { HeatMap } from '@nivo/heatmap' -import { Box, Heading } from 'ooni-components' - -import { Debug } from '../Debug' -import { - colorNormal, - colorAnomaly, - colorConfirmed, - colorError -} from '../../colors' - -const getHeatMapData = (data, yAxis) => { - /* - 'IT': { - probe_cc: 'IT', - '2021-03-01': { - anomaly_count: 111, - measurement_count: 200, - confirmed_count: 10 - }, - '2021-03-02': { - anomaly_count: 12, - ... - } - } - */ - const keys = new Set() - const indexBy = yAxis // probe_cc or input - const groupByXAxis = data.reduce((acc, item) => { - const { measurement_start_day, ...restOfItem } = item - const key = item[yAxis] - const value = restOfItem - const date = measurement_start_day // xAxis - keys.add(date) - - if (!(key in acc)) { - // Add a new (day,count) to that - acc[key] = {} - acc[key][yAxis] = key - } - acc[key][date] = value - - return acc - }, {}) - const reducedData = Object.keys(groupByXAxis).map(k => groupByXAxis[k]) - return [ - [...keys], - reducedData, - indexBy - ] -} - -const CustomHeatMapCell = ({ - // data, - value, - x, - y, - width, - height, - // color, - opacity, - borderWidth, - borderColor, - textColor, - // enableLabel, - // onClick, - onHover, - onLeave, - // theme -}) => { - const barsData = [ - ['ok_count', colorNormal], - ['failure_count', colorError], - ['confirmed_count', colorConfirmed], - ['anomaly_count', colorAnomaly], - ] - - let prevHeight = 0 - - return ( - - {value !== undefined && barsData.map(([barLabel, barColor], index) => { - const barHeight = height * (value[barLabel] / value['measurement_count']) // ((index + 1) / 10) - const barY = prevHeight === 0 ? (height * -0.5) : (height * -0.5) + prevHeight - prevHeight = prevHeight + barHeight - return ( - - ) - })} - {value === undefined && ( - - {'❤️'} - - )} - - ) -} - -CustomHeatMapCell.propTypes = { - borderColor: PropTypes.string, - borderWidth: PropTypes.number, - height: PropTypes.number, - onHover: PropTypes.func, - onLeave: PropTypes.func, - opacity: PropTypes.number, - textColor: PropTypes.string, - value: PropTypes.any, - width: PropTypes.number, - x: PropTypes.number, - y: PropTypes.number -} - -const HeatMapCell = React.memo(CustomHeatMapCell) - -const HEIGHT_MULTIPLIER = 30 - -export const HeatmapChart = ({ data, query }) => { - - const yAxis = query.axis_y - const [keys, shapedData, indexBy] = getHeatMapData(data, yAxis) - return ( - - - `\n Total - ${value.measurement_count}\n Anomalies - ${value.anomaly_count}\n Confirmed - ${value.confirmed_count}\n Ok - ${value.ok_count}\n Failures - ${value.failure_count}` - } - forceSquare={true} - isInteractive={true} - animate={false} - /> - - - Processed Data -
-            {JSON.stringify(shapedData, null, 2)}
-          
- API Response Data -
-            {JSON.stringify(data, null, 2)}
-          
-
-
-
- ) -} -HeatmapChart.propTypes = { - data: PropTypes.arrayOf( - PropTypes.shape({ - anomaly_count: PropTypes.number, - confirmed_count: PropTypes.number, - failure_count: PropTypes.number, - measurement_count: PropTypes.number, - ok_count: PropTypes.number, - measurement_start_day: PropTypes.string, - probe_cc: PropTypes.string - }) - ), - query: PropTypes.shape({ - axis_x: PropTypes.string, - axis_y: PropTypes.string, - since: PropTypes.string, - until: PropTypes.string, - test_name: PropTypes.string, - input: PropTypes.string, - probe_cc: PropTypes.string, - category_code: PropTypes.string, - }) -} diff --git a/components/aggregation/mat/Help.js b/components/aggregation/mat/Help.js index 3067d1d81..e36a842cd 100644 --- a/components/aggregation/mat/Help.js +++ b/components/aggregation/mat/Help.js @@ -1,44 +1,41 @@ +import FormattedMarkdown from 'components/FormattedMarkdown' import { DetailsBox } from 'components/measurement/DetailsBox' -import { Flex, Box, Text, Heading } from 'ooni-components' -import { MdHelp } from 'react-icons/md' -import styled from 'styled-components' - import { getCategoryCodesMap } from 'components/utils/categoryCodes' -import FormattedMarkdown from 'components/FormattedMarkdown' +import { MdHelp } from 'react-icons/md' import { FormattedMessage } from 'react-intl' -const Row = styled(Flex)` - padding: 8px 0px; - /* odd/even stying - uses index prop given to Row from Array.map */ - ${props => Number(props.index) % 2 && 'background: inherit;'}; - border-bottom: 1px solid ${props => props.theme.colors.gray2}; -` - -const Name = styled(Box).attrs({ - fontWeight: 'bold' -})`` - const boxTitle = ( - +
- - +
+ +
+
) const Help = () => { return ( - - - {[...getCategoryCodesMap().values()].map(({ code, name, description }, i) => ( - - - - - ))} - + +
+ {[...getCategoryCodesMap().values()].map( + ({ code, name, description }, i) => ( +
+
+ +
+
+ +
+
+ ), + )} +
) } -export default Help \ No newline at end of file +export default Help diff --git a/components/aggregation/mat/MATContext.js b/components/aggregation/mat/MATContext.js index edd8b8712..60902e1ba 100644 --- a/components/aggregation/mat/MATContext.js +++ b/components/aggregation/mat/MATContext.js @@ -1,5 +1,11 @@ -import { createContext, useContext, useCallback, useEffect, useState } from 'react' import { useRouter } from 'next/router' +import { + createContext, + useCallback, + useContext, + useEffect, + useState, +} from 'react' export const MATStateContext = createContext() @@ -17,7 +23,11 @@ export const defaultMATContext = { tooltipIndex: [-1, ''], } -export const MATContextProvider = ({ children, queryParams, ...initialContext }) => { +export const MATContextProvider = ({ + children, + queryParams, + ...initialContext +}) => { const [state, setState] = useState({ ...defaultMATContext, ...initialContext, @@ -32,10 +42,16 @@ export const MATContextProvider = ({ children, queryParams, ...initialContext }) setState((state) => partial ? Object.assign({}, state, updates) - : Object.assign({}, state, defaultMATContext, initialContext, updates) + : Object.assign( + {}, + state, + defaultMATContext, + initialContext, + updates, + ), ) }, - [initialContext] + [initialContext], ) useEffect(() => { @@ -43,7 +59,9 @@ export const MATContextProvider = ({ children, queryParams, ...initialContext }) }, [MATquery]) return ( - + {children} ) diff --git a/components/aggregation/mat/NoCharts.js b/components/aggregation/mat/NoCharts.js index a3457c989..995dfc61a 100644 --- a/components/aggregation/mat/NoCharts.js +++ b/components/aggregation/mat/NoCharts.js @@ -1,31 +1,20 @@ -import { Flex, Box, Heading, Text } from 'ooni-components' import { FormattedMessage } from 'react-intl' -import styled from 'styled-components' - -const StyledBox = styled(Box)` -text-align: center; -background-color: ${props => props.theme.colors.gray2}; -` export const NoCharts = ({ message }) => { return ( - - +
+
- - +
+
- - {message && - - - - {message} - - - } - - - + {message && ( +
+ +
{message}
+
+ )} +
+
) } diff --git a/components/aggregation/mat/RowChart.js b/components/aggregation/mat/RowChart.js index 632f0dc60..8a03ce343 100644 --- a/components/aggregation/mat/RowChart.js +++ b/components/aggregation/mat/RowChart.js @@ -1,28 +1,27 @@ -import React, { useCallback, useEffect, useMemo, useState, useRef } from 'react' -import PropTypes from 'prop-types' -import { Box, Flex } from 'ooni-components' import { ResponsiveBar as Bar } from '@nivo/bar' import { useTooltip } from '@nivo/tooltip' - +import PropTypes from 'prop-types' +import { + createElement, + memo, + useCallback, + useEffect, + useMemo, + useState, +} from 'react' +import { useIntl } from 'react-intl' +import { getDirection } from '../../withIntl' import { CustomBarItem } from './CustomBarItem' -import { CustomToolTip, InvisibleTooltip, themeForInvisibleTooltip } from './CustomTooltip' -import { colorMap } from './colorMap' +import { + CustomToolTip, + InvisibleTooltip, + themeForInvisibleTooltip, +} from './CustomTooltip' import { useMATContext } from './MATContext' +import { colorMap } from './colorMap' import { getXAxisTicks } from './timeScaleXAxis' -import { defineMessages, useIntl } from 'react-intl' -import styled from 'styled-components' -import { getDirection } from '../../withIntl' - -const keys = [ - 'anomaly_count', - 'confirmed_count', - 'failure_count', - 'ok_count', -] -const StyledFlex = styled(Flex)` - direction: ltr; -` +const keys = ['anomaly_count', 'confirmed_count', 'failure_count', 'ok_count'] const colorFunc = (d) => colorMap[d.id] || '#ccc' @@ -33,7 +32,12 @@ const formatXAxisValues = (value, query, intl) => { if (query.axis_x === 'measurement_start_day' && Date.parse(value)) { if (query.time_grain === 'hour') { const dateTime = new Date(value) - return new Intl.DateTimeFormat(intl.locale, { dateStyle: 'short', timeStyle: 'short', timeZone: 'UTC', hourCycle: 'h23' }).format(dateTime) + return new Intl.DateTimeFormat(intl.locale, { + dateStyle: 'short', + timeStyle: 'short', + timeZone: 'UTC', + hourCycle: 'h23', + }).format(dateTime) } } else { return value @@ -44,16 +48,16 @@ const chartProps1D = (query, intl) => ({ colors: colorFunc, indexScale: { type: 'band', - round: false + round: false, }, margin: { top: 30, right: 20, bottom: 80, - left: 70 + left: 70, }, padding: 0.3, - borderColor: { from: 'color', modifiers: [ [ 'darker', 1.6 ] ] }, + borderColor: { from: 'color', modifiers: [['darker', 1.6]] }, axisTop: null, axisRight: null, axisBottom: { @@ -63,7 +67,12 @@ const chartProps1D = (query, intl) => ({ legendPosition: 'middle', legendOffset: 70, tickValues: getXAxisTicks(query), - legend: query.axis_x ? intl.formatMessage({id: `MAT.Form.Label.AxisOption.${query.axis_x}`, defaultMessage: '' }) : '', + legend: query.axis_x + ? intl.formatMessage({ + id: `MAT.Form.Label.AxisOption.${query.axis_x}`, + defaultMessage: '', + }) + : '', format: (values) => formatXAxisValues(values, query, intl), }, axisLeft: { @@ -71,11 +80,11 @@ const chartProps1D = (query, intl) => ({ tickPadding: 5, tickRotation: 0, legendPosition: 'middle', - legendOffset: -60 + legendOffset: -60, }, labelSkipWidth: 80, labelSkipHeight: 20, - labelTextColor: { from: 'color', modifiers: [ [ 'darker', 1.6 ] ] }, + labelTextColor: { from: 'color', modifiers: [['darker', 1.6]] }, animate: true, motionStiffness: 90, motionDamping: 15, @@ -86,14 +95,14 @@ const chartProps2D = (query) => ({ // margin: chartMargins, padding: 0.3, - borderColor: { from: 'color', modifiers: [ [ 'darker', 1.6 ] ] }, + borderColor: { from: 'color', modifiers: [['darker', 1.6]] }, colors: colorFunc, axisTop: null, axisRight: { enable: true, tickSize: 5, tickPadding: 5, - tickValues: 2 + tickValues: 2, }, axisBottom: null, axisLeft: null, @@ -101,23 +110,29 @@ const chartProps2D = (query) => ({ enableGridY: true, indexScale: { type: 'band', - round: false + round: false, }, labelSkipWidth: 100, labelSkipHeight: 100, - labelTextColor: { from: 'color', modifiers: [ [ 'darker', 1.6 ] ] }, + labelTextColor: { from: 'color', modifiers: [['darker', 1.6]] }, // We send the `showTooltip` boolean into the barComponent to control visibility of tooltip motionConfig: { - duration: 1 + duration: 1, }, animate: false, isInteractive: true, layers: barLayers, }) -const RowChart = ({ data, indexBy, label, height, rowIndex /* width, first, last */}) => { +const RowChart = ({ + data, + indexBy, + label, + height, + rowIndex /* width, first, last */, +}) => { const intl = useIntl() - const [ query, updateMATContext ] = useMATContext() + const [query, updateMATContext] = useMATContext() const { tooltipIndex } = query const { showTooltipFromEvent, hideTooltip } = useTooltip() @@ -125,19 +140,21 @@ const RowChart = ({ data, indexBy, label, height, rowIndex /* width, first, last hideTooltip() }, [hideTooltip]) - const handleClick = useCallback(({ data }) => { - const column = data[query.axis_x] - updateMATContext({ tooltipIndex: [rowIndex, column]}, true) - showTooltipFromEvent( - React.createElement(CustomToolTip, { - data: data, - onClose - }), - event, - 'top' - ) - - }, [onClose, query.axis_x, rowIndex, showTooltipFromEvent, updateMATContext]) + const handleClick = useCallback( + ({ data }) => { + const column = data[query.axis_x] + updateMATContext({ tooltipIndex: [rowIndex, column] }, true) + showTooltipFromEvent( + createElement(CustomToolTip, { + data: data, + onClose, + }), + event, + 'top', + ) + }, + [onClose, query.axis_x, rowIndex, showTooltipFromEvent, updateMATContext], + ) // Load the chart with an empty data to avoid // react-spring from working on the actual data during @@ -145,24 +162,21 @@ const RowChart = ({ data, indexBy, label, height, rowIndex /* width, first, last // real data, which appears quick enough with animation disabled const [chartData, setChartData] = useState([]) useEffect(() => { - let animation = setTimeout(() => setChartData(data), 1) + const animation = setTimeout(() => setChartData(data), 1) return () => { clearTimeout(animation) } }, [data]) - const chartProps = useMemo(() => { return label === undefined ? chartProps1D(query, intl) : chartProps2D(query) }, [intl, label, query]) return ( - - {label && - {label} - } - +
+ {label &&
{label}
} +
- - +
+
) } RowChart.propTypes = { - data: PropTypes.arrayOf(PropTypes.shape({ - anomaly_count: PropTypes.number, - confirmed_count: PropTypes.number, - failure_count: PropTypes.number, - input: PropTypes.string, - measurement_count: PropTypes.number, - measurement_start_day: PropTypes.string, - ok_count: PropTypes.number, - })), + data: PropTypes.arrayOf( + PropTypes.shape({ + anomaly_count: PropTypes.number, + confirmed_count: PropTypes.number, + failure_count: PropTypes.number, + input: PropTypes.string, + measurement_count: PropTypes.number, + measurement_start_day: PropTypes.string, + ok_count: PropTypes.number, + }), + ), height: PropTypes.number, indexBy: PropTypes.string, label: PropTypes.node, @@ -200,4 +216,4 @@ RowChart.propTypes = { RowChart.displayName = 'RowChart' -export default React.memo(RowChart) +export default memo(RowChart) diff --git a/components/aggregation/mat/StackedBarChart.js b/components/aggregation/mat/StackedBarChart.js index 01aded68b..fb7711fa3 100644 --- a/components/aggregation/mat/StackedBarChart.js +++ b/components/aggregation/mat/StackedBarChart.js @@ -1,26 +1,16 @@ -import React from 'react' import PropTypes from 'prop-types' -import { Flex } from 'ooni-components' -import styled from 'styled-components' import { useIntl } from 'react-intl' - import GridChart, { prepareDataForGridChart } from './GridChart' import { NoCharts } from './NoCharts' -const ChartContainer = styled(Flex)` - position: relative; - // border: 2px solid ${props => props.theme.colors.gray1}; - // padding: 16px; -` - export const StackedBarChart = ({ data, query }) => { const intl = useIntl() - try { - const [gridData, rows ] = prepareDataForGridChart(data, query, intl.locale) + try { + const [gridData, rows] = prepareDataForGridChart(data, query, intl.locale) return ( - +
{ height={500} noLabels={true} /> - +
) } catch (e) { - return () + return } } @@ -42,7 +32,7 @@ StackedBarChart.propTypes = { result: PropTypes.array, }), loadTime: PropTypes.number, - url: PropTypes.string + url: PropTypes.string, }), - query: PropTypes.object + query: PropTypes.object, } diff --git a/components/aggregation/mat/TableView.js b/components/aggregation/mat/TableView.js index d3da068b9..b262c5755 100644 --- a/components/aggregation/mat/TableView.js +++ b/components/aggregation/mat/TableView.js @@ -1,5 +1,4 @@ -import { Flex } from 'ooni-components' -import React, { useMemo, useRef, useState } from 'react' +import { useMemo, useRef, useState } from 'react' import { useIntl } from 'react-intl' import Filters from './Filters' @@ -7,10 +6,19 @@ import GridChart, { prepareDataForGridChart } from './GridChart' const prepareDataforTable = (data, query, locale) => { const table = [] - - const [reshapedData, rows, rowLabels] = prepareDataForGridChart(data, query, locale) - const countKeys = ['anomaly_count', 'confirmed_count', 'failure_count', 'measurement_count'] + const [reshapedData, rows, rowLabels] = prepareDataForGridChart( + data, + query, + locale, + ) + + const countKeys = [ + 'anomaly_count', + 'confirmed_count', + 'failure_count', + 'measurement_count', + ] for (const [key, rowData] of reshapedData) { const row = { [query.axis_y]: key, @@ -21,8 +29,8 @@ const prepareDataforTable = (data, query, locale) => { measurement_count: 0, } - rowData.forEach(d => { - countKeys.forEach(countKey => { + rowData.forEach((d) => { + countKeys.forEach((countKey) => { row[countKey] = row[countKey] + d[countKey] }) }) @@ -46,7 +54,7 @@ const TableView = ({ data, query, showFilters = true }) => { // to then filter out rows based on `selectedRows` generated by the table // - tableData: this has aggregated counts and labels for each row to be // displayed in GridChart. It allows to easily filter and sort aggregate data - // - indexes - + // - indexes - const [reshapedData, tableData, rowKeys, rowLabels] = useMemo(() => { try { return prepareDataforTable(data, query, intl.locale) @@ -58,22 +66,22 @@ const TableView = ({ data, query, showFilters = true }) => { const [dataForCharts, setDataForCharts] = useState(noRowsSelected) return ( - - {showFilters && +
+ {showFilters && ( - } + )} - +
) } -export default TableView \ No newline at end of file +export default TableView diff --git a/components/aggregation/mat/VirtualRows.js b/components/aggregation/mat/VirtualRows.js index fb9f2e14d..8e5a5a7f4 100644 --- a/components/aggregation/mat/VirtualRows.js +++ b/components/aggregation/mat/VirtualRows.js @@ -1,33 +1,20 @@ import PropTypes from 'prop-types' -import React, { useCallback } from 'react' -import { Flex } from 'ooni-components' -import styled from 'styled-components' +import { useCallback, useRef } from 'react' -import RowChart from './RowChart' import { defaultRangeExtractor, useVirtual } from 'react-virtual' +import RowChart from './RowChart' const GRID_ROW_CSS_SELECTOR = 'outerListElement' const ROW_HEIGHT = 70 const retainMountedRows = false -const FlexWithNoScrollbar = styled(Flex)` - width: 100%; - overflow-y: auto; -` -const StickyRow = styled.div` - position: sticky; - top: 0px; - background: white; - z-index: 1; -` - const useKeepMountedRangeExtractor = () => { - const renderedRef = React.useRef(new Set()) + const renderedRef = useRef(new Set()) - const rangeExtractor = React.useCallback(range => { + const rangeExtractor = useCallback((range) => { renderedRef.current = new Set([ ...renderedRef.current, - ...defaultRangeExtractor(range) + ...defaultRangeExtractor(range), ]) return Array.from(renderedRef.current) }, []) @@ -35,9 +22,16 @@ const useKeepMountedRangeExtractor = () => { return rangeExtractor } -export const VirtualRows = ({ data, rows, rowLabels, gridHeight, indexBy, tooltipIndex, xAxis = null }) => { - - const parentRef = React.useRef() +export const VirtualRows = ({ + data, + rows, + rowLabels, + gridHeight, + indexBy, + tooltipIndex, + xAxis = null, +}) => { + const parentRef = useRef() const keepMountedRangeExtractor = useKeepMountedRangeExtractor() const keyExtractor = useCallback((index) => rows[index], [rows]) @@ -49,13 +43,15 @@ export const VirtualRows = ({ data, rows, rowLabels, gridHeight, indexBy, toolti paddingStart: 62, // for the sticky x-axis overscan: 0, keyExtractor, - rangeExtractor: retainMountedRows ? keepMountedRangeExtractor : defaultRangeExtractor + rangeExtractor: retainMountedRows + ? keepMountedRangeExtractor + : defaultRangeExtractor, }) return ( - - {xAxis && - - {xAxis} - - } + {xAxis &&
{xAxis}
} {rowVirtualizer.virtualItems.map((virtualRow) => (
))}
-
+ ) } VirtualRows.propTypes = { diff --git a/components/aggregation/mat/XAxis.js b/components/aggregation/mat/XAxis.js index cb94c9f8c..725107f87 100644 --- a/components/aggregation/mat/XAxis.js +++ b/components/aggregation/mat/XAxis.js @@ -1,34 +1,27 @@ import { ResponsiveBar } from '@nivo/bar' -import { Box, Flex } from 'ooni-components' -import styled from 'styled-components' import { useMATContext } from './MATContext' -import { getXAxisTicks } from './timeScaleXAxis' import { useIntl } from 'react-intl' import { getDirection } from '../../withIntl' - -const StyledFlex = styled(Flex)` - direction: ltr; -` +import { getXAxisTicks } from './timeScaleXAxis' export const XAxis = ({ data }) => { const { locale } = useIntl() - const [ query ] = useMATContext() + const [query] = useMATContext() const xAxisTickValues = getXAxisTicks(query, 30) - const xAxisMargins = {right: 50, left: 0, top: 60, bottom: 0} + const xAxisMargins = { right: 50, left: 0, top: 60, bottom: 0 } const axisTop = { enable: true, tickSize: 5, tickPadding: 5, tickRotation: getDirection(locale) === 'ltr' ? -45 : 315, - tickValues: xAxisTickValues + tickValues: xAxisTickValues, } return ( - - - - +
+
+
{ padding={0.3} indexScale={{ type: 'band', - round: false + round: false, }} layers={['axes']} axisTop={axisTop} @@ -45,7 +38,7 @@ export const XAxis = ({ data }) => { axisRight={null} animate={false} /> - - +
+
) } diff --git a/components/aggregation/mat/colorMap.js b/components/aggregation/mat/colorMap.js index 37c77b172..eba8c826e 100644 --- a/components/aggregation/mat/colorMap.js +++ b/components/aggregation/mat/colorMap.js @@ -1,9 +1,9 @@ -import { theme } from 'ooni-components' +import { colors } from 'ooni-components' export const colorMap = { - 'confirmed_count': theme.colors.red7, - 'anomaly_count': theme.colors.yellow5, - 'failure_count': theme.colors.gray4, - 'ok_count': theme.colors.green5, - 'measurement_count': theme.colors.blue5, + confirmed_count: colors.red['700'], + anomaly_count: colors.yellow['500'], + failure_count: colors.gray['400'], + ok_count: colors.green['500'], + measurement_count: colors.blue['500'], } diff --git a/components/aggregation/mat/computations.js b/components/aggregation/mat/computations.js index d462a8b61..1bff2c37c 100644 --- a/components/aggregation/mat/computations.js +++ b/components/aggregation/mat/computations.js @@ -1,18 +1,18 @@ -import { getCategoryCodesMap } from '../../utils/categoryCodes' -import { getLocalisedRegionName, localisedCountries } from 'utils/i18nCountries' import dayjs from 'services/dayjs' +import { localisedCountries } from 'utils/i18nCountries' +import { getCategoryCodesMap } from '../../utils/categoryCodes' const categoryCodesMap = getCategoryCodesMap() export function getDatesBetween(startDate, endDate, timeGrain) { const dateSet = new Set() - var currentDate = startDate + let currentDate = startDate while (currentDate < endDate) { if (timeGrain === 'hour') { let startOfDay = dayjs(currentDate).utc().startOf('day') const nextDay = startOfDay.add(1, 'day') while (startOfDay.toDate() < nextDay.toDate()) { - dateSet.add(startOfDay.toISOString().split('.')[0] + 'Z') + dateSet.add(`${startOfDay.toISOString().split('.')[0]}Z`) startOfDay = startOfDay.utc().add(1, 'hours') } currentDate = dayjs(currentDate).utc().add(1, 'day') @@ -32,29 +32,35 @@ export function getDatesBetween(startDate, endDate, timeGrain) { return dateSet } -export function fillRowHoles (data, query, locale) { +export function fillRowHoles(data, query, locale) { const newData = [...data] let domain = null - switch(query.axis_x) { + switch (query.axis_x) { case 'measurement_start_day': - domain = getDatesBetween(new Date(query.since), new Date(query.until), query.time_grain) + domain = getDatesBetween( + new Date(query.since), + new Date(query.until), + query.time_grain, + ) break case 'category_code': domain = [...getCategoryCodesMap().keys()] break case 'probe_cc': - domain = localisedCountries(locale).map(cc => cc.iso3166_alpha2) + domain = localisedCountries(locale).map((cc) => cc.iso3166_alpha2) break default: - throw new Error(`x-axis: ${query.axis_x}. Please select a valid value for X-Axis.`) + throw new Error( + `x-axis: ${query.axis_x}. Please select a valid value for X-Axis.`, + ) } - const colsInRow = newData.map(i => i[query.axis_x]) - const missingCols = [...domain].filter(x => !colsInRow.includes(x)) + const colsInRow = newData.map((i) => i[query.axis_x]) + const missingCols = [...domain].filter((x) => !colsInRow.includes(x)) - const sampleDataPoint = {...newData[0]} + const sampleDataPoint = { ...newData[0] } // Add empty datapoints for columns where measurements are not available missingCols.forEach((col) => { @@ -74,19 +80,21 @@ export function fillRowHoles (data, query, locale) { return newData } -export function fillDataHoles (data, query) { +export function fillDataHoles(data, query) { // Object transformation, works like Array.map // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/fromEntries#object_transformations const newData = new Map( - Object.entries(data) - .map(([ key, rowData ]) => [ key, fillRowHoles(rowData, query) ]) + Object.entries(data).map(([key, rowData]) => [ + key, + fillRowHoles(rowData, query), + ]), ) return newData } export const sortRows = (a, b, type, locale = 'en') => { - switch(type) { + switch (type) { // case 'probe_cc': // return new Intl.Collator(locale).compare(getLocalisedRegionName(a, locale), getLocalisedRegionName(b, locale)) default: diff --git a/components/aggregation/mat/labels.js b/components/aggregation/mat/labels.js index 378aff99c..748e67cdc 100644 --- a/components/aggregation/mat/labels.js +++ b/components/aggregation/mat/labels.js @@ -1,19 +1,13 @@ import PropTypes from 'prop-types' -import { Box } from 'ooni-components' - -import { testNames } from '../../test-info' -import { getCategoryCodesMap } from '../../utils/categoryCodes' import { getLocalisedRegionName } from 'utils/i18nCountries' +import { testNames } from '../../test-info' const InputRowLabel = ({ input }) => { const truncatedInput = input return ( - +
{truncatedInput} - +
) } @@ -21,14 +15,12 @@ InputRowLabel.propTypes = { input: PropTypes.string, } -const categoryCodesMap = getCategoryCodesMap() - const blockingTypeLabels = { '': '', - 'dns': 'DNS Tampering', + dns: 'DNS Tampering', 'http-diff': 'HTTP Diff', 'http-failure': 'HTTP Failure', - 'tcp_ip': 'TCP/IP Blocking' + tcp_ip: 'TCP/IP Blocking', } export const getRowLabel = (key, yAxis, locale = 'en') => { @@ -41,16 +33,16 @@ export const getRowLabel = (key, yAxis, locale = 'en') => { return messages[`CategoryCode.${key}.Name`] case 'input': case 'domain': - return () + return case 'blocking_type': return blockingTypeLabels[key] ?? key case 'probe_asn': return `AS${key}` case 'test_name': - return Object.keys(testNames).includes(key) ? - messages[testNames[key].id] : - key + return Object.keys(testNames).includes(key) + ? messages[testNames[key].id] + : key default: return key } -} \ No newline at end of file +} diff --git a/components/aggregation/mat/timeScaleXAxis.js b/components/aggregation/mat/timeScaleXAxis.js index 7c96f14ad..9f5f9a842 100644 --- a/components/aggregation/mat/timeScaleXAxis.js +++ b/components/aggregation/mat/timeScaleXAxis.js @@ -1,47 +1,56 @@ import { scaleUtc } from 'd3-scale' -import { getDatesBetween } from './computations' import dayjs from 'services/dayjs' - +import { getDatesBetween } from './computations' const defaultCount = 20 const getIntervalTicks = (data, count = defaultCount) => { - if (!(data && data.length)) return [] + if (!data?.length) return [] const start = data[0] const end = data[data.length - 1] const intervalType = 'week' const intervalCount = Math.floor(dayjs(end).diff(start, intervalType)) return data.reduce((accum, point, index) => { - const divisor = Math.ceil(intervalCount / count) - if (index % divisor === 0) - accum.push(point) - return accum + const divisor = Math.ceil(intervalCount / count) + if (index % divisor === 0) accum.push(point) + return accum }, []) } - -export function getXAxisTicks (query, count = defaultCount) { - +export function getXAxisTicks(query, count = defaultCount) { if (query.axis_x === 'measurement_start_day') { - const dateDomain = [...getDatesBetween(new Date(query.since), new Date(query.until), query.time_grain)].map(d => new Date(d)) - const xScale = scaleUtc().domain([dateDomain[0], dateDomain[dateDomain.length-1]]) - const xAxisTickValues = dateDomain.length < 30 ? dateDomain : [ - ...xScale.ticks(count), - ] - + const dateDomain = [ + ...getDatesBetween( + new Date(query.since), + new Date(query.until), + query.time_grain, + ), + ].map((d) => new Date(d)) + const xScale = scaleUtc().domain([ + dateDomain[0], + dateDomain[dateDomain.length - 1], + ]) + const xAxisTickValues = + dateDomain.length < 30 ? dateDomain : [...xScale.ticks(count)] + if (query.time_grain === 'hour') { - return Array.from(xAxisTickValues).map(d => d.toISOString().split('.')[0] + 'Z') - } else if (query.time_grain === 'week') { - return dateDomain.length < 30 ? - Array.from(dateDomain).map(d => d.toISOString().split('T')[0]) : - getIntervalTicks(dateDomain.map((d) => d.toISOString().split('T')[0]), count) + return Array.from(xAxisTickValues).map( + (d) => `${d.toISOString().split('.')[0]}Z`, + ) + } + if (query.time_grain === 'week') { + return dateDomain.length < 30 + ? Array.from(dateDomain).map((d) => d.toISOString().split('T')[0]) + : getIntervalTicks( + dateDomain.map((d) => d.toISOString().split('T')[0]), + count, + ) } - return Array.from(xAxisTickValues).map(d => d.toISOString().split('T')[0]) + return Array.from(xAxisTickValues).map((d) => d.toISOString().split('T')[0]) } return count } - diff --git a/components/as/Calendar.js b/components/as/Calendar.js index ff87375bd..708074d92 100644 --- a/components/as/Calendar.js +++ b/components/as/Calendar.js @@ -1,18 +1,18 @@ import { ResponsiveCalendar } from '@nivo/calendar' import { add, compareDesc, startOfToday, startOfYear } from 'date-fns' -import { Flex, theme } from 'ooni-components' -import React, { useState } from 'react' -import styled from 'styled-components' +import { colors } from 'ooni-components' +import { memo, useState } from 'react' import { getRange } from 'utils' -const StyledCalendar = styled.div` -height: 180px; -` -const { colors } = theme -const chartColors = [colors.blue2, colors.blue4, colors.blue5, colors.blue7] +const chartColors = [ + colors.blue['200'], + colors.blue['400'], + colors.blue['500'], + colors.blue['700'], +] -const findColor = number => { - if (number === 0) return colors.gray1 +const findColor = (number) => { + if (number === 0) return colors.gray['100'] if (number <= 50) return chartColors[0] if (number <= 500) return chartColors[1] if (number <= 5000) return chartColors[2] @@ -20,10 +20,10 @@ const findColor = number => { } 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'}, + { 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) => { @@ -40,74 +40,73 @@ const dateRange = (startDate, endDate) => { return dates } -const backfillData = data => { +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})) + return range.map((r) => data.find((d) => d.day === r) || { value: 0, day: r }) } -const Calendar = React.memo(function Calendar({data}) { +const Calendar = memo(function Calendar({ data }) { const currentYear = new Date().getFullYear() const firstMeasurementYear = Number(data[0].day.split('-')[0]) const yearsOptions = getRange(firstMeasurementYear, currentYear) - const [ selectedYear, setSelectedYear ] = useState(currentYear) + const [selectedYear, setSelectedYear] = useState(currentYear) const calendarData = backfillData(data) return ( <> - +
findColor(value)} margin={{ top: 20, right: 0, bottom: 0, left: 20 }} monthBorderColor="#ffffff" dayBorderWidth={2} dayBorderColor="#ffffff" /> - - - - {colorLegend.map(item => ( - - +
+
+
+ {colorLegend.map((item) => ( + + {item.range} ))} - - - {yearsOptions.map(year => ( +
+
+ {yearsOptions.map((year) => ( setSelectedYear(year)} > {year} ))} - - +
+
) }) -export default Calendar \ No newline at end of file +export default Calendar diff --git a/components/as/Form.js b/components/as/Form.js index 607a923cc..745a8b811 100644 --- a/components/as/Form.js +++ b/components/as/Form.js @@ -1,9 +1,9 @@ -import { useEffect, useRef, useState, useMemo } from 'react' -import { useForm, Controller } from 'react-hook-form' -import { Box, Flex, Input } from 'ooni-components' +import { format } from 'date-fns' +import { Input } from 'ooni-components' +import { useEffect, useRef, useState } from 'react' +import { Controller, useForm } from 'react-hook-form' import { useIntl } from 'react-intl' import dayjs from 'services/dayjs' -import { format } from 'date-fns' import DateRangePicker from '../DateRangePicker' @@ -47,12 +47,12 @@ const Form = ({ onSubmit, since, until }) => { return (
- - - - +
+
+
+
( { /> )} /> - - +
+
( { /> )} /> - - +
+
{showDatePicker && ( { close={() => setShowDatePicker(false)} /> )} - - +
+
) } diff --git a/components/as/Loader.js b/components/as/Loader.js index 47f874d03..7d7ebaefe 100644 --- a/components/as/Loader.js +++ b/components/as/Loader.js @@ -1,4 +1,3 @@ -import React from 'react' import ContentLoader from 'react-content-loader' const Loader = (props) => ( diff --git a/components/axios-plugins.js b/components/axios-plugins.js index 08b70a453..a94bad9eb 100644 --- a/components/axios-plugins.js +++ b/components/axios-plugins.js @@ -11,7 +11,6 @@ export const axiosResponseTime = (instance) => { }) } - /* https://stackoverflow.com/questions/62186171/measure-network-latency-in-react-native/62257712#62257712 const axiosTiming = (instance) => { diff --git a/components/colors.js b/components/colors.js index 653fee7c5..f39c50196 100644 --- a/components/colors.js +++ b/components/colors.js @@ -1,7 +1,7 @@ -import { theme } from 'ooni-components' +import { colors } from 'ooni-components' -export const colorNormal = theme.colors.green7 -export const colorError = theme.colors.gray4 -export const colorConfirmed = theme.colors.red8 -export const colorAnomaly = theme.colors.yellow8 -export const colorEmpty = theme.colors.gray3 +export const colorNormal = colors.green['700'] +export const colorError = colors.gray['400'] +export const colorConfirmed = colors.red['800'] +export const colorAnomaly = colors.yellow['800'] +export const colorEmpty = colors.gray['300'] diff --git a/components/country/Apps.js b/components/country/Apps.js index dcd487c8f..d70286293 100644 --- a/components/country/Apps.js +++ b/components/country/Apps.js @@ -1,12 +1,10 @@ +import Chart from 'components/Chart' +import { useRouter } from 'next/router' import { useMemo } from 'react' import { FormattedMessage, useIntl } from 'react-intl' -import { Text, Box } from 'ooni-components' - +import FormattedMarkdown from '../FormattedMarkdown' import SectionHeader from './SectionHeader' import { SimpleBox } from './boxes' -import Chart from 'components/Chart' -import FormattedMarkdown from '../FormattedMarkdown' -import { useRouter } from 'next/router' const messagingTestNames = ['signal', 'telegram', 'whatsapp', 'facebook_messenger'] const circumventionTestNames = ['psiphon', 'tor', 'torsf'] @@ -38,18 +36,18 @@ const ChartsContainer = () => { return ( <> - +
- - +
+
- +
) } @@ -62,9 +60,9 @@ const AppsSection = () => ( - +
- +
diff --git a/components/country/Calendar.js b/components/country/Calendar.js index a41050574..666ea0736 100644 --- a/components/country/Calendar.js +++ b/components/country/Calendar.js @@ -1,12 +1,11 @@ import { ResponsiveCalendar } from '@nivo/calendar' import CTABox from 'components/CallToActionBox' import SpinLoader from 'components/vendor/SpinLoader' -import { Box, Flex, theme } from 'ooni-components' +import { colors } 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' @@ -39,14 +38,15 @@ const CallToActionBox = () => { ) } -const StyledCalendar = styled.div` -height: 180px; -` -const { colors } = theme -const chartColors = [colors.blue2, colors.blue4, colors.blue5, colors.blue7] +const chartColors = [ + colors.blue['200'], + colors.blue['400'], + colors.blue['500'], + colors.blue['700'], +] const findColor = number => { - if (number === 0) return colors.gray1 + if (number === 0) return colors.gray['100'] if (number <= 50) return chartColors[0] if (number <= 500) return chartColors[1] if (number <= 5000) return chartColors[2] @@ -121,64 +121,52 @@ const Calendar = React.memo(function Calendar({ startYear }) { ) return ( - - {isLoading && } +
+ {isLoading &&
} {!!calendarData.length && - +
findColor(value)} margin={{ top: 20, right: 0, bottom: 0, left: 20 }} monthBorderColor="#ffffff" dayBorderWidth={2} dayBorderColor="#ffffff" /> - +
} {!calendarData.length && !isLoading && } {error && - +
Error: {JSON.stringify(error)} - +
} - - +
+
{colorLegend.map(item => ( - - + + {item.range} ))} - - +
+
{yearsOptions.map(year => ( setSelectedYear(year)} > {year} ))} - - - +
+
+
) }) diff --git a/components/country/ConfirmedBlockedCategory.js b/components/country/ConfirmedBlockedCategory.js index 87922d99e..7c9b18f1c 100644 --- a/components/country/ConfirmedBlockedCategory.js +++ b/components/country/ConfirmedBlockedCategory.js @@ -1,8 +1,6 @@ import { CategoryBadge } from 'components/Badge' import { DetailsBox } from 'components/measurement/DetailsBox' -import { getCategoryCodesMap } from 'components/utils/categoryCodes' import { useRouter } from 'next/router' -import { Box, Flex, Heading } from 'ooni-components' import { memo, useMemo } from 'react' import { FormattedMessage, useIntl } from 'react-intl' import { MATFetcher } from 'services/fetchers' @@ -15,27 +13,30 @@ const swrOptions = { const ConfirmedBlockedCategory = () => { const router = useRouter() - const { query: { countryCode, since, until } } = router + const { + query: { countryCode, since, until }, + } = router const intl = useIntl() - const categoryCodeMap = getCategoryCodesMap() - - const query = useMemo(() => ({ - probe_cc: countryCode, - since, - until, - test_name: 'web_connectivity', - axis_x: 'category_code' - }), [countryCode, since, until]) + const query = useMemo( + () => ({ + probe_cc: countryCode, + since, + until, + test_name: 'web_connectivity', + axis_x: 'category_code', + }), + [countryCode, since, until], + ) const prepareDataForBadge = (categoriesData) => { - return categoriesData.filter(category => category.confirmed_count > 0) + return categoriesData.filter((category) => category.confirmed_count > 0) } const { data, error } = useSWR( since && until ? new URLSearchParams(query).toString() : null, MATFetcher, - swrOptions + swrOptions, ) const blockedCategoriesData = useMemo(() => { @@ -49,38 +50,47 @@ const ConfirmedBlockedCategory = () => { }, [data]) return ( - - {intl.formatMessage({id: 'Country.Websites.ConfirmedBlockedCategories'})} - - {(!blockedCategoriesData && !error) ? ( -
Loading ...
- ) : ( - blockedCategoriesData === null || blockedCategoriesData.length === 0 ? ( - - ) : ( - - {blockedCategoriesData && blockedCategoriesData.map(category => ( - - ))} - - ) - )} -
- {error && - -
- Error: {error.message} - - {JSON.stringify(error, null, 2)} - -
- }/> - } - -
+
+

+ {intl.formatMessage({ + id: 'Country.Websites.ConfirmedBlockedCategories', + })} +

+ <> + {!blockedCategoriesData && !error ? ( +
Loading ...
+ ) : blockedCategoriesData === null || + blockedCategoriesData.length === 0 ? ( +
+ +
+ ) : ( +
+ {blockedCategoriesData?.map((category) => ( + + ))} +
+ )} + + {error && ( + +
+ + Error: {error.message} + +
{JSON.stringify(error, null, 2)}
+
+ + } + /> + )} +
) } diff --git a/components/country/CountryContext.js b/components/country/CountryContext.js index 21c8ac17c..9b90b88e7 100644 --- a/components/country/CountryContext.js +++ b/components/country/CountryContext.js @@ -1,20 +1,22 @@ -import React, { useContext } from 'react' import PropTypes from 'prop-types' +import { createContext, useContext } from 'react' // TODO: Maybe add period information to update data in all sections when // period filter is updated in one of the sections -export const CountryContext = React.createContext() +export const CountryContext = createContext() export const CountryContextProvider = ({ countryCode, countryName, - children + children, }) => ( - + {children} ) @@ -22,7 +24,7 @@ export const CountryContextProvider = ({ CountryContextProvider.propTypes = { countryCode: PropTypes.string.isRequired, countryName: PropTypes.string.isRequired, - children: PropTypes.any + children: PropTypes.any, } /* Custom Hook to use CountryContext */ diff --git a/components/country/CountryDetails.js b/components/country/CountryDetails.js index 78613489a..5d06c63d8 100644 --- a/components/country/CountryDetails.js +++ b/components/country/CountryDetails.js @@ -10,48 +10,37 @@ import Overview from 'components/country/Overview' import PageNavMenu from 'components/country/PageNavMenu' import WebsitesSection from 'components/country/Websites' import { useRouter } from 'next/router' -import { - Box, - Container, - Flex, - Heading -} from 'ooni-components' import { useCallback, useEffect, useMemo, useState } from 'react' import { useIntl } from 'react-intl' import dayjs from 'services/dayjs' -import styled from 'styled-components' import useScrollPosition from '/hooks/useScrollPosition' import { getLocalisedRegionName } from '/utils/i18nCountries' - -const AnimatedFlex = styled(Flex)` - transition: all 0.5s ease; -` - const Header = ({ countryCode, countryName }) => { const scrollPosition = useScrollPosition() - const miniHeader = scrollPosition >= 150 ? true : false + const miniHeader = scrollPosition >= 150 return ( - - - - - - + // py={ miniHeader ? 0 : 4} +
+ +
+ {/* fontSize={miniHeader ? 2 : 4} */} +

{countryName} - - - - +

+
+ +
) } -const AnimatedHeading = styled(Heading)` - transition: all 0.5s ease; -` - -const CountryDetails = ({ countryCode, overviewStats, reports, coverageDataSSR }) => { +const CountryDetails = ({ + countryCode, + overviewStats, + reports, + coverageDataSSR, +}) => { const intl = useIntl() const countryName = getLocalisedRegionName(countryCode, intl.locale) const [newData, setNewData] = useState(false) @@ -62,9 +51,13 @@ const CountryDetails = ({ countryCode, overviewStats, reports, coverageDataSSR } const today = dayjs.utc().add(1, 'day') const monthAgo = dayjs.utc(today).subtract(1, 'month') - return { - since: dayjs(query.since, 'YYYY-MM-DD', true).isValid() ? query.since : monthAgo.format('YYYY-MM-DD'), - until: dayjs(query.until, 'YYYY-MM-DD', true).isValid() ? query.until : today.format('YYYY-MM-DD') + return { + since: dayjs(query.since, 'YYYY-MM-DD', true).isValid() + ? query.since + : monthAgo.format('YYYY-MM-DD'), + until: dayjs(query.until, 'YYYY-MM-DD', true).isValid() + ? query.until + : today.format('YYYY-MM-DD'), } }, [query]) @@ -82,25 +75,28 @@ const CountryDetails = ({ countryCode, overviewStats, reports, coverageDataSSR } } }, []) - const fetchTestCoverageData = useCallback((testGroupList) => { - - const fetcher = async (testGroupList) => { - let client = axios.create({baseURL: process.env.NEXT_PUBLIC_OONI_API}) // eslint-disable-line - const result = await client.get('/api/_/test_coverage', { - params: { - 'probe_cc': countryCode, - 'test_groups': testGroupList - } - }) - // TODO: Use React.createContext to pass along data and methods - setNewData({ - networkCoverage: result.data.network_coverage, - testCoverage: result.data.test_coverage - }) - } - fetcher(testGroupList) - - }, [countryCode, setNewData]) + const fetchTestCoverageData = useCallback( + (testGroupList) => { + const fetcher = async (testGroupList) => { + const client = axios.create({ + baseURL: process.env.NEXT_PUBLIC_OONI_API, + }) // eslint-disable-line + const result = await client.get('/api/_/test_coverage', { + params: { + probe_cc: countryCode, + test_groups: testGroupList, + }, + }) + // TODO: Use React.createContext to pass along data and methods + setNewData({ + networkCoverage: result.data.network_coverage, + testCoverage: result.data.test_coverage, + }) + } + fetcher(testGroupList) + }, + [countryCode, setNewData], + ) // Sync page URL params with changes from form values const onSubmit = ({ since, until }) => { @@ -119,20 +115,29 @@ const CountryDetails = ({ countryCode, overviewStats, reports, coverageDataSSR } } } - const { testCoverage, networkCoverage } = newData !== false ? newData : coverageDataSSR + const { testCoverage, networkCoverage } = + newData !== false ? newData : coverageDataSSR return ( <> - + - +
- +
- - - - +
+
+
+ - - - +
+
+
) } diff --git a/components/country/CountryHead.js b/components/country/CountryHead.js index f78e20d2e..e7ae1a211 100644 --- a/components/country/CountryHead.js +++ b/components/country/CountryHead.js @@ -1,47 +1,52 @@ -import React from 'react' import Head from 'next/head' import { useIntl } from 'react-intl' -const CountryHead = ({ - countryName, - measurementCount, - networkCount -}) => { +const CountryHead = ({ countryName, measurementCount, networkCount }) => { const intl = useIntl() return ( - {intl.formatMessage({ id: 'Country.Meta.Title'}, { countryName })} + + {intl.formatMessage({ id: 'Country.Meta.Title' }, { countryName })} + - ) } diff --git a/components/country/Overview.js b/components/country/Overview.js index 499d886ba..6e0d801cd 100644 --- a/components/country/Overview.js +++ b/components/country/Overview.js @@ -1,7 +1,5 @@ import BlockText from 'components/BlockText' import Calendar from 'components/country/Calendar' -import { Box, Heading, Link, Text } from 'ooni-components' -import React from 'react' import { FormattedMessage, useIntl } from 'react-intl' import FormattedMarkdown from '../FormattedMarkdown' import { useCountry } from './CountryContext' @@ -10,12 +8,17 @@ import { BoxWithTitle } from './boxes' const ooniBlogBaseURL = 'https://ooni.org' -const FeaturedArticle = ({link, title}) => ( - - +const FeaturedArticle = ({ link, title }) => ( + ) const Overview = ({ @@ -23,7 +26,7 @@ const Overview = ({ networkCount, measurementCount, measuredSince, - featuredArticles = [] + featuredArticles = [], }) => { const intl = useIntl() const { countryCode } = useCountry() @@ -31,45 +34,45 @@ const Overview = ({ return ( <> - - + + {/* */} - + {/* */} - - - - - - - +

+ +

+
+ +
- - }> - { - (featuredArticles.length === 0) - ? - :
    - {featuredArticles.map((article, index) => ( -
  • - -
  • - ))} -
- } + } + > + {featuredArticles.length === 0 ? ( + + ) : ( +
    + {featuredArticles.map((article, index) => ( +
  • + +
  • + ))} +
+ )}
{/* Highlight Box */} diff --git a/components/country/PageNavMenu.js b/components/country/PageNavMenu.js index b6acbe68b..b433b66bb 100644 --- a/components/country/PageNavMenu.js +++ b/components/country/PageNavMenu.js @@ -1,40 +1,21 @@ -import React, { useState } from 'react' import PropTypes from 'prop-types' -import { Flex, Box, Link } from 'ooni-components' -import styled from 'styled-components' -import { FormattedMessage } from 'react-intl' +import { useState } from 'react' import { MdExpandLess } from 'react-icons/md' +import { FormattedMessage } from 'react-intl' import SocialButtons from '../SocialButtons' const HideInLargeScreens = ({ children }) => ( - - {children} - +
{children}
) const PageNavItem = ({ link, children }) => ( - - {children} - + ) -const ToggleIcon = styled(MdExpandLess).attrs({ - -})` - cursor: pointer; - background-color: ${props => props.theme.colors.gray3}; - border-radius: 50%; - transform: ${props => props.open ? 'rotate(0deg)': 'rotate(180deg)'}; - transition: transform 0.1s linear; -` - const PageNavMenu = ({ countryCode }) => { const [isOpen, setOpen] = useState(true) @@ -42,33 +23,41 @@ const PageNavMenu = ({ countryCode }) => { <> {/* Show a trigger to open and close the nav menu, but hide it on desktops */} - setOpen(!isOpen)} /> + setOpen(!isOpen)} + /> - - {isOpen && - - - - - - - - - - - - - } - - - - + {/* width={[1, 'unset']} */} +
+ {isOpen && ( +
+ + + + + + + + + + + + +
+ )} +
+ +
+
) } PageNavMenu.propTypes = { - countryCode: PropTypes.string + countryCode: PropTypes.string, } export default PageNavMenu diff --git a/components/country/SectionHeader.js b/components/country/SectionHeader.js index 71cbdd641..d70a0befe 100644 --- a/components/country/SectionHeader.js +++ b/components/country/SectionHeader.js @@ -1,30 +1,24 @@ -import { Flex } from 'ooni-components' -import styled from 'styled-components' +const SectionHeader = ({ children }) => ( +
+ {children} +
+) -const SectionHeader = styled(Flex)` - border-bottom: 1px solid ${props => props.theme.colors.gray3}; -` +SectionHeader.Title = ({ children, ...props }) => ( + + {children} + +) -SectionHeader.defaultProps = { - pb: 3, - my: 4, - flexWrap: 'wrap' -} - -SectionHeader.Title = styled.a` - color: ${props => props.theme.colors.blue5}; - font-size: 42px; - font-weight: 600; - /* To compenstate for the sticky navigation bar - :target selector applies only the element with id that matches - the current URL fragment (e.g '/#Overview') */ - :target::before { - content: ' '; - display: block; - width: 0; - /* NOTE: This is the combined height of the NavBar and PageNavMenu */ - height: 140px; - } -` +// /* To compenstate for the sticky navigation bar +// :target selector applies only the element with id that matches +// the current URL fragment (e.g '/#Overview') */ +// :target::before { +// content: ' '; +// display: block; +// width: 0; +// /* NOTE: This is the combined height of the NavBar and PageNavMenu */ +// height: 140px; +// } export default SectionHeader diff --git a/components/country/Tooltip.js b/components/country/Tooltip.js index fb8a2d6c5..5ce0eacef 100644 --- a/components/country/Tooltip.js +++ b/components/country/Tooltip.js @@ -1,11 +1,6 @@ -import React from 'react' +import { VictoryLabel, VictoryTooltip } from 'victory' -import { - VictoryLabel, - VictoryTooltip -} from 'victory' - -import { theme } from 'ooni-components' +import { colors } from 'ooni-components' import { firaSans } from '../../pages/_app' const Tooltip = (props) => ( @@ -14,17 +9,17 @@ const Tooltip = (props) => ( labelComponent={ } flyoutStyle={{ strokeWidth: 0, - fill: theme.colors.gray8, + fill: colors.gray['800'], padding: 2, - pointerEvents: 'none' + pointerEvents: 'none', }} /> ) diff --git a/components/country/Websites.js b/components/country/Websites.js index 48a34ffdd..1573e8db4 100644 --- a/components/country/Websites.js +++ b/components/country/Websites.js @@ -1,48 +1,49 @@ -import React, {useCallback, useMemo} from 'react' -import { useIntl, FormattedMessage } from 'react-intl' -import { Box, Text } from 'ooni-components' import ChartCountry from 'components/Chart' -import SectionHeader from './SectionHeader' -import { SimpleBox } from './boxes' +import { useRouter } from 'next/router' +import { useMemo } from 'react' +import { FormattedMessage } from 'react-intl' import FormattedMarkdown from '../FormattedMarkdown' import ConfirmedBlockedCategory from './ConfirmedBlockedCategory' -import { useRouter } from 'next/router' +import SectionHeader from './SectionHeader' +import { SimpleBox } from './boxes' const WebsitesSection = ({ countryCode }) => { const router = useRouter() - const { query: { since, until } } = router + const { + query: { since, until }, + } = router - const query = useMemo(() => ({ - axis_y: 'domain', - axis_x: 'measurement_start_day', - probe_cc: countryCode, - since, - until, - test_name: 'web_connectivity', - time_grain: 'day', - }), [countryCode, since, until]) + const query = useMemo( + () => ({ + axis_y: 'domain', + axis_x: 'measurement_start_day', + probe_cc: countryCode, + since, + until, + test_name: 'web_connectivity', + time_grain: 'day', + }), + [countryCode, since, until], + ) return ( <> - - + + - - - +
+ +
- - - +
+ +
) } -export default WebsitesSection \ No newline at end of file +export default WebsitesSection diff --git a/components/country/boxes.js b/components/country/boxes.js index 1cb59f1d6..d15b07d9c 100644 --- a/components/country/boxes.js +++ b/components/country/boxes.js @@ -1,35 +1,19 @@ -import React from 'react' import PropTypes from 'prop-types' -import styled from 'styled-components' -import { Box, Container, Text } from 'ooni-components' - -const StyledBox = styled(Box)` - border: 1px solid ${props => props.theme.colors.gray4}; -` export const SimpleBox = ({ children }) => ( - - {children} - +
{children}
) -SimpleBox.propTypes = { - children: PropTypes.node -} - export const BoxWithTitle = ({ title, children }) => ( - - - {title} +
+
+
{title}
{children} - - +
+
) BoxWithTitle.propTypes = { - title: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.element - ]).isRequired, - children: PropTypes.node + title: PropTypes.oneOfType([PropTypes.string, PropTypes.element]).isRequired, + children: PropTypes.node, } diff --git a/components/dashboard/Charts.js b/components/dashboard/Charts.js index a47d01e31..8d60b2977 100644 --- a/components/dashboard/Charts.js +++ b/components/dashboard/Charts.js @@ -1,15 +1,16 @@ -import React, { useMemo } from 'react' -import { Flex, Box, Heading } from 'ooni-components' -import { useRouter } from 'next/router' -import useSWR from 'swr' import axios from 'axios' +import { useRouter } from 'next/router' +import { memo, useMemo } from 'react' import { useIntl } from 'react-intl' +import useSWR from 'swr' -import GridChart, { prepareDataForGridChart } from '../aggregation/mat/GridChart' +import GridChart, { + prepareDataForGridChart, +} from '../aggregation/mat/GridChart' import { MATContextProvider } from '../aggregation/mat/MATContext' import { axiosResponseTime } from '../axios-plugins' -import { testNames } from '../test-info' import { DetailsBox } from '../measurement/DetailsBox' +import { testNames } from '../test-info' const swrOptions = { revalidateOnFocus: false, @@ -22,42 +23,52 @@ axiosResponseTime(axios) // TODO export from mat.js const fetcher = (query) => { const reqUrl = `${baseURL}/api/v1/aggregation?${query}` - return axios.get(reqUrl).then(r => { - if (!r?.data?.result) { - const error = new Error(`Request ${reqUrl} did not contain expected result`) - error.data = r - throw error - } - return { - data: r.data.result, - loadTime: r.loadTime, - url: r.config.url - } - }).catch(e => { - console.log(e) - e.message = e?.request?.response ?? e.message - throw e - }) + return axios + .get(reqUrl) + .then((r) => { + if (!r?.data?.result) { + const error = new Error( + `Request ${reqUrl} did not contain expected result`, + ) + error.data = r + throw error + } + return { + data: r.data.result, + loadTime: r.loadTime, + url: r.config.url, + } + }) + .catch((e) => { + console.log(e) + e.message = e?.request?.response ?? e.message + throw e + }) } const fixedQuery = { axis_x: 'measurement_start_day', axis_y: 'probe_cc', } -const Chart = React.memo(function Chart({ testName }) { +const Chart = memo(function Chart({ testName }) { const intl = useIntl() - const { query: {probe_cc, since, until} } = useRouter() + const { + query: { probe_cc, since, until }, + } = useRouter() // Construct a `query` object that matches the router.query // used by MAT, because in this case axis_x, axis_y are not part // of router.query - const query = useMemo(() => ({ - ...fixedQuery, - test_name: testName, - since: since, - until: until, - time_grain: 'day' - }), [since, testName, until]) + const query = useMemo( + () => ({ + ...fixedQuery, + test_name: testName, + since: since, + until: until, + time_grain: 'day', + }), + [since, testName, until], + ) const apiQuery = useMemo(() => { const qs = new URLSearchParams(query).toString() @@ -65,38 +76,44 @@ const Chart = React.memo(function Chart({ testName }) { }, [query]) const { data, error } = useSWR( - () => since && until ? apiQuery : null, + () => (since && until ? apiQuery : null), fetcher, - swrOptions + swrOptions, ) - + const [chartData, rowKeys, rowLabels] = useMemo(() => { if (!data) { return [null, 0] } - let chartData = data.data.sort((a, b) => (new Intl.Collator(intl.locale).compare(a.probe_cc, b.probe_cc))) + let chartData = data.data.sort((a, b) => + new Intl.Collator(intl.locale).compare(a.probe_cc, b.probe_cc), + ) const selectedCountries = probe_cc?.length > 1 ? probe_cc.split(',') : [] if (selectedCountries.length > 0) { - chartData = chartData.filter(d => selectedCountries.includes(d.probe_cc)) + chartData = chartData.filter((d) => + selectedCountries.includes(d.probe_cc), + ) } - const [reshapedData, rowKeys, rowLabels] = prepareDataForGridChart(chartData, query, intl.locale) + const [reshapedData, rowKeys, rowLabels] = prepareDataForGridChart( + chartData, + query, + intl.locale, + ) return [reshapedData, rowKeys, rowLabels] - }, [data, probe_cc, query, intl]) - const headerOptions = { probe_cc: false, subtitle: false } return ( - - {testNames[testName].name} - - {(!chartData && !error) ? ( -
{intl.formatMessage({id: 'General.Loading'})}
+
+

{testNames[testName].name}

+
+ {!chartData && !error ? ( +
{intl.formatMessage({ id: 'General.Loading' })}
) : ( )} - - {error && - -
- {intl.formatMessage({id: 'General.Error'})}: {error.message} - - {JSON.stringify(error, null, 2)} - -
- }/> - } - - +
+ {error && ( + +
+ + + {intl.formatMessage({ id: 'General.Error' })}:{' '} + {error.message} + + +
{JSON.stringify(error, null, 2)}
+
+ + } + /> + )} +
) }) const testsOnPage = ['psiphon', 'tor', 'torsf'] const ChartsContainer = () => { - return ( - testsOnPage.map(testName => ( - - - - )) - ) + return testsOnPage.map((testName) => ( + + + + )) } -export default ChartsContainer \ No newline at end of file +export default ChartsContainer diff --git a/components/dashboard/Form.js b/components/dashboard/Form.js index 9cba051d3..fe368dd45 100644 --- a/components/dashboard/Form.js +++ b/components/dashboard/Form.js @@ -1,8 +1,8 @@ +import { format } from 'date-fns' +import { Input, MultiSelect } from 'ooni-components' import { useEffect, useMemo, useState } from 'react' -import { useForm, Controller } from 'react-hook-form' -import { Box, Flex, Input, MultiSelect } from 'ooni-components' +import { Controller, useForm } from 'react-hook-form' import { useIntl } from 'react-intl' -import { format } from 'date-fns' import { getLocalisedRegionName } from '../../utils/i18nCountries' import DateRangePicker from '../DateRangePicker' @@ -17,8 +17,10 @@ export const Form = ({ onChange, query, availableCountries }) => { label: getLocalisedRegionName(cc, intl.locale), value: cc, })) - .sort((a, b) => new Intl.Collator(intl.locale).compare(a.label, b.label)), - [availableCountries, intl] + .sort((a, b) => + new Intl.Collator(intl.locale).compare(a.label, b.label), + ), + [availableCountries, intl], ) const query2formValues = useMemo(() => { @@ -26,7 +28,9 @@ export const Form = ({ onChange, query, availableCountries }) => { return { since: query?.since, until: query?.until, - probe_cc: countryOptions.filter(country => countriesInQuery.includes(country.value)), + probe_cc: countryOptions.filter((country) => + countriesInQuery.includes(country.value), + ), } }, [countryOptions, query]) @@ -41,7 +45,9 @@ export const Form = ({ onChange, query, availableCountries }) => { search: intl.formatMessage({ id: 'ReachabilityDash.Form.Label.CountrySelect.SearchPlaceholder', }), - selectAll: intl.formatMessage({ id: 'ReachabilityDash.Form.Label.CountrySelect.SelectAll' }), + selectAll: intl.formatMessage({ + id: 'ReachabilityDash.Form.Label.CountrySelect.SelectAll', + }), selectAllFiltered: intl.formatMessage({ id: 'ReachabilityDash.Form.Label.CountrySelect.SelectAllFiltered', }), @@ -50,7 +56,7 @@ export const Form = ({ onChange, query, availableCountries }) => { }), // 'create': 'Create', }), - [intl] + [intl], ) const { control, getValues, watch, setValue, reset } = useForm({ @@ -66,7 +72,10 @@ export const Form = ({ onChange, query, availableCountries }) => { return { since, until, - probe_cc: probe_cc.length > 0 ? probe_cc.map((d) => d.value).join(',') : undefined, + probe_cc: + probe_cc.length > 0 + ? probe_cc.map((d) => d.value).join(',') + : undefined, } } @@ -88,31 +97,32 @@ export const Form = ({ onChange, query, availableCountries }) => { useEffect(() => { const subscription = watch((value, { name, type }) => { - if (name === 'probe_cc' && type === 'change') onChange(cleanedUpData(getValues())) + if (name === 'probe_cc' && type === 'change') + onChange(cleanedUpData(getValues())) }) return () => subscription.unsubscribe() }, [watch, getValues]) return (
- - +
+
( + render={({ field }) => ( )} - name='probe_cc' + name="probe_cc" control={control} /> - - - - +
+
+
+
{ /> )} /> - - +
+
{ /> )} /> - - +
+
{showDatePicker && ( setShowDatePicker(false)} /> )} - - +
+
) } diff --git a/components/dashboard/MetaTags.js b/components/dashboard/MetaTags.js index a0500050a..a8b72218f 100644 --- a/components/dashboard/MetaTags.js +++ b/components/dashboard/MetaTags.js @@ -3,25 +3,22 @@ import { useIntl } from 'react-intl' export const MetaTags = () => { const intl = useIntl() - const title = intl.formatMessage({ id: 'ReachabilityDash.Heading.CircumventionTools' }) - const description = intl.formatMessage({ id: 'ReachabilityDash.Meta.Description' }) + const title = intl.formatMessage({ + id: 'ReachabilityDash.Heading.CircumventionTools', + }) + const description = intl.formatMessage({ + id: 'ReachabilityDash.Meta.Description', + }) return ( {title} - + - + ) -} \ No newline at end of file +} diff --git a/components/domain/Form.js b/components/domain/Form.js index 6b738332e..cbfc77cdf 100644 --- a/components/domain/Form.js +++ b/components/domain/Form.js @@ -1,13 +1,13 @@ -import { useEffect, useState, useMemo } from 'react' -import { useForm, Controller } from 'react-hook-form' -import { Box, Flex, Input, Select } from 'ooni-components' +import { format } from 'date-fns' +import { Input, Select } from 'ooni-components' +import { useEffect, useMemo, useState } from 'react' +import { Controller, useForm } from 'react-hook-form' import { useIntl } from 'react-intl' import dayjs from 'services/dayjs' -import { format } from 'date-fns' +import { useRouter } from 'next/router' import { getLocalisedRegionName } from 'utils/i18nCountries' import DateRangePicker from '../DateRangePicker' -import { useRouter } from 'next/router' const tomorrow = dayjs.utc().add(1, 'day').format('YYYY-MM-DD') const lastMonthToday = dayjs.utc().subtract(30, 'day').format('YYYY-MM-DD') @@ -61,13 +61,13 @@ const Form = ({ onSubmit, availableCountries = [] }) => { const subscription = watch((value, { name, type }) => { if ( value[name] !== query[name] && - dayjs(value['since'], 'YYYY-MM-DD', true).isValid() && - dayjs(value['until'], 'YYYY-MM-DD', true).isValid() + dayjs(value.since, 'YYYY-MM-DD', true).isValid() && + dayjs(value.until, 'YYYY-MM-DD', true).isValid() ) { onSubmit({ - since: value['since'], - until: value['until'], - probe_cc: value['probe_cc'], + since: value.since, + until: value.until, + probe_cc: value.probe_cc, }) } }) @@ -92,10 +92,10 @@ const Form = ({ onSubmit, availableCountries = [] }) => { return (
- - - - +
+
+
+
{ /> )} /> - - +
+
{ /> )} /> - - +
+
{showDatePicker && ( { close={() => setShowDatePicker(false)} /> )} - - +
+
{ )} /> - - +
+
) } diff --git a/components/findings/FindingDisplay.js b/components/findings/FindingDisplay.js index 8a7e08a7e..aea01ef9c 100644 --- a/components/findings/FindingDisplay.js +++ b/components/findings/FindingDisplay.js @@ -1,12 +1,11 @@ -import { Heading, Flex, Box, Text } from 'ooni-components' import { Badge } from 'components/Badge' import Flag from 'components/Flag' -import Markdown from 'markdown-to-jsx' import { MATChartWrapper } from 'components/MATChart' -import { getLocalisedRegionName } from 'utils/i18nCountries' +import Markdown from 'markdown-to-jsx' +import Link from 'next/link' import { useIntl } from 'react-intl' import { formatLongDate } from 'utils' -import NLink from 'next/link' +import { getLocalisedRegionName } from 'utils/i18nCountries' const FormattedMarkdown = ({ children }) => { return ( @@ -28,39 +27,59 @@ const FindingDisplay = ({ incident }) => { const intl = useIntl() const reportedBy = incident?.reported_by - const formattedCreationDate = incident?.create_time && formatLongDate(incident?.create_time, intl.locale) - const listOfNetworks = incident?.ASNs?.map((as) => ({`AS${as}`})).reduce((prev, curr) => (prev ? [prev, ', ', curr] : curr), null) + const formattedCreationDate = + incident?.create_time && formatLongDate(incident?.create_time, intl.locale) + const listOfNetworks = incident?.ASNs?.map((as) => ( + {`AS${as}`} + )).reduce((prev, curr) => (prev ? [prev, ', ', curr] : curr), null) return ( <> - - {incident?.title} - +

{incident?.title}

{!!incident?.CCs?.length && ( - +
- +

{getLocalisedRegionName(incident.CCs[0], intl.locale)} - - +

+
)} - {incident?.start_time && formatLongDate(incident?.start_time, intl.locale)} - {incident?.end_time ? formatLongDate(incident?.end_time, intl.locale) : 'ongoing'} +
+ {incident?.start_time && + formatLongDate(incident?.start_time, intl.locale)}{' '} + -{' '} + {incident?.end_time + ? formatLongDate(incident?.end_time, intl.locale) + : 'ongoing'} +
{!!incident?.tags?.length && ( - +
{incident.tags.map((tag) => ( - +
{tag} - +
))} - +
+ )} +
+ {intl.formatMessage( + { id: 'Findings.Display.CreatedByOn' }, + { reportedBy, formattedDate: formattedCreationDate }, + )} +
+ {!!incident?.ASNs?.length && ( +
+ {intl.formatMessage( + { id: 'Findings.Display.Network' }, + { listOfNetworks }, + )} +
)} - {intl.formatMessage({id: 'Findings.Display.CreatedByOn'}, {reportedBy, formattedDate: formattedCreationDate})} - {!!incident?.ASNs?.length && - - {intl.formatMessage({id: 'Findings.Display.Network'}, { listOfNetworks })} - - } - {incident?.text && {incident.text}} +
+ {incident?.text && ( + {incident.text} + )} +
) } diff --git a/components/findings/Form.js b/components/findings/Form.js index af93c25c5..dc493b216 100644 --- a/components/findings/Form.js +++ b/components/findings/Form.js @@ -1,18 +1,29 @@ -import { Input, Textarea, Button, Flex, Box, Checkbox, Modal, MultiSelectCreatable, MultiSelect, Text} from 'ooni-components' -import { useForm, Controller } from 'react-hook-form' -import { useIntl } from 'react-intl' -import { useCallback, useState } from 'react' -import { HtmlValidate, StaticConfigLoader, defineMetadata } from 'html-validate/browser' import { yupResolver } from '@hookform/resolvers/yup' +import { + HtmlValidate, + StaticConfigLoader, + defineMetadata, +} from 'html-validate/browser' +import { + Checkbox, + Input, + Modal, + MultiSelect, + MultiSelectCreatable, + Textarea, +} from 'ooni-components' +import { useCallback, useState } from 'react' +import { Controller, useForm } from 'react-hook-form' +import { useIntl } from 'react-intl' import { localisedCountries } from 'utils/i18nCountries' +import * as yup from 'yup' +import useUser from '../../hooks/useUser' import FindingDisplay from './FindingDisplay' import { testNames } from '/components/test-info' -import useUser from '../../hooks/useUser' -import * as yup from 'yup' const elements = [ defineMetadata({ - 'MAT': { + MAT: { void: false, attributes: { link: { @@ -25,83 +36,94 @@ const elements = [ boolean: false, omit: false, }, - } + }, }, - }) + }), ] const loader = new StaticConfigLoader({ rules: { - 'void-style': ['error', { 'style': 'selfclose' }], + 'void-style': ['error', { style: 'selfclose' }], 'void-content': 'error', - 'element-case': ['error', {'style': 'uppercase'}], - 'element-name': ['error', {'whitelist': ['MAT']}], - 'attr-quotes': ['error', {'style': 'auto', 'unquoted': false}], + 'element-case': ['error', { style: 'uppercase' }], + 'element-name': ['error', { whitelist: ['MAT'] }], + 'attr-quotes': ['error', { style: 'auto', unquoted: false }], 'element-required-attributes': 'error', 'attribute-allowed-values': 'error', - 'attribute-boolean-style': 'error', - 'attribute-empty-style': 'error', + 'attribute-boolean-style': 'error', + 'attribute-empty-style': 'error', }, elements, }) const htmlvalidate = new HtmlValidate(loader) -const schema = yup - .object({ - title: yup.string().required(), - email_address: yup.string().required(), - reported_by: yup.string().required(), - short_description: yup.string().required(), - ASNs: yup.array().test({ - name: 'ASNsError', - message: 'Only numeric values allowed', - test: (val) => val.every((v) => !isNaN(v.value)), - }), - start_time: yup.string().required(), - end_time: yup.string().nullable().test({ +const schema = yup.object({ + title: yup.string().required(), + email_address: yup.string().required(), + reported_by: yup.string().required(), + short_description: yup.string().required(), + ASNs: yup.array().test({ + name: 'ASNsError', + message: 'Only numeric values allowed', + test: (val) => val.every((v) => !isNaN(v.value)), + }), + start_time: yup.string().required(), + end_time: yup + .string() + .nullable() + .test({ name: 'EndTimeError', message: 'Must be after start time', test: (val, testContext) => { if (val) - return new Date(testContext.parent.start_time).getTime() < new Date(val).getTime() + return ( + new Date(testContext.parent.start_time).getTime() < + new Date(val).getTime() + ) return true }, }), - text: yup.string().required().test({ + text: yup + .string() + .required() + .test({ test: async (value, context) => { const validation = await htmlvalidate.validateString(value) if (!validation.valid) { - const message = validation.results.map((obj) => obj.messages.map((m) => m.message).join(', ')).join(', ') + const message = validation.results + .map((obj) => obj.messages.map((m) => m.message).join(', ')) + .join(', ') return context.createError({ message, path: 'text' }) } else { return true } }, }), - }) +}) const Form = ({ defaultValues, onSubmit }) => { const intl = useIntl() const { user } = useUser() defaultValues = { - ...defaultValues, + ...defaultValues, CCs: defaultValues.CCs.map((cc) => { - const ccObj = localisedCountries(intl.locale).find((co) => (co.iso3166_alpha2 === cc)) + const ccObj = localisedCountries(intl.locale).find( + (co) => co.iso3166_alpha2 === cc, + ) return { - label: ccObj.localisedCountryName, - value: ccObj.iso3166_alpha2 + label: ccObj.localisedCountryName, + value: ccObj.iso3166_alpha2, } }), test_names: defaultValues.test_names.map((tn) => ({ - label: testNames[tn] ? intl.formatMessage({id: testNames[tn].id}) : tn, - value: tn - } - )), - tags: defaultValues.tags.map((t) => ({label: t, value: t})), - ASNs: defaultValues.ASNs.map((as) => ({label: as, value: as})), - domains: defaultValues.domains.map((d) => ({label: d, value: d})) + label: testNames[tn] ? intl.formatMessage({ id: testNames[tn].id }) : tn, + value: tn, + })), + tags: defaultValues.tags.map((t) => ({ label: t, value: t })), + ASNs: defaultValues.ASNs.map((as) => ({ label: as, value: as })), + domains: defaultValues.domains.map((d) => ({ label: d, value: d })), } const { handleSubmit, control, getValues, formState } = useForm({ @@ -115,12 +137,15 @@ const Form = ({ defaultValues, onSubmit }) => { const testNamesOptions = Object.entries(testNames).map(([k, v]) => ({ value: k, - label: intl.formatMessage({id: v.id}), + label: intl.formatMessage({ id: v.id }), })) const sortedCountries = localisedCountries(intl.locale) .sort((a, b) => - new Intl.Collator(intl.locale).compare(a.localisedCountryName, b.localisedCountryName) + new Intl.Collator(intl.locale).compare( + a.localisedCountryName, + b.localisedCountryName, + ), ) .map(({ iso3166_alpha2, localisedCountryName }) => ({ value: iso3166_alpha2, @@ -143,12 +168,20 @@ const Form = ({ defaultValues, onSubmit }) => { return onSubmit({ ...incident, start_time: `${incident.start_time}T00:00:00Z`, - ...(incident.end_time ? { end_time: `${incident.end_time}T00:00:00Z` } : {end_time: null}), - test_names: incident.test_names.length ? incident.test_names.map((test_name) => test_name.value) : [], + ...(incident.end_time + ? { end_time: `${incident.end_time}T00:00:00Z` } + : { end_time: null }), + test_names: incident.test_names.length + ? incident.test_names.map((test_name) => test_name.value) + : [], CCs: incident.CCs.length ? incident.CCs.map((cc) => cc.value) : [], tags: incident.tags.length ? incident.tags.map((t) => t.value) : [], - ASNs: incident.ASNs.length ? incident.ASNs.map((as) => Number(as.value)) : [], - domains: incident.domains.length ? incident.domains.map((d) => d.value) : [], + ASNs: incident.ASNs.length + ? incident.ASNs.map((as) => Number(as.value)) + : [], + domains: incident.domains.length + ? incident.domains.map((d) => d.value) + : [], }) } @@ -161,43 +194,77 @@ const Form = ({ defaultValues, onSubmit }) => { width="100%" onHideClick={() => setShowPreview(!showPreview)} > - +
- +
-
handleSubmit(submit)(e).catch((e) => setSubmitError(e.message))}> - {user?.role === 'admin' && - + + handleSubmit(submit)(e).catch((e) => setSubmitError(e.message)) + } + > + {user?.role === 'admin' && ( +
} + render={({ field }) => ( + + )} /> - - } +
+ )} ( - + )} /> ( - + )} /> ( - + )} /> - - +
+
{ )} /> - - +
+
{ {...field} type="date" error={errors?.end_time?.message} - label={intl.formatMessage({ id: 'Findings.Form.EndTime.Label' })} + label={intl.formatMessage({ + id: 'Findings.Form.EndTime.Label', + })} id="end_time" /> )} /> - - +
+
- - } + render={({ field }) => ( + + )} /> } + render={({ field }) => ( + + )} /> } + render={({ field }) => ( + + )} /> } + render={({ field }) => ( + + )} /> } + render={({ field }) => ( + + )} /> ( - + )} /> { /> )} /> - - - {submitError && - - {intl.formatMessage({id: 'Measurement.Feedback.Failure'})} -
+ + + {submitError && ( +
+ {intl.formatMessage({ id: 'Measurement.Feedback.Failure' })} +
Error: {submitError} - - } +
+ )} ) diff --git a/components/findings/LoginRequiredModal.js b/components/findings/LoginRequiredModal.js index cd8d5a3e4..79cc71284 100644 --- a/components/findings/LoginRequiredModal.js +++ b/components/findings/LoginRequiredModal.js @@ -1,16 +1,7 @@ -import { Modal, Container, Flex, Button, Heading } from 'ooni-components' +import { useRouter } from 'next/router' +import { Modal } from 'ooni-components' import { useIntl } from 'react-intl' -import styled from 'styled-components' import useUser from '../../hooks/useUser' -import { useRouter } from 'next/router' - -const StyledModal = styled(Modal)` - border-radius: 8px; - box-shadow: 4px 4px 10px black; - svg { - display: none; - } -` const LoginRequiredModal = ({ show }) => { const intl = useIntl() @@ -23,19 +14,23 @@ const LoginRequiredModal = ({ show }) => { } return ( - - - - + +
+
+

{intl.formatMessage({ id: 'Findings.LoginRequiredModal.Title' })} - - -

+
+ - - + +
+
) } -export default LoginRequiredModal \ No newline at end of file +export default LoginRequiredModal diff --git a/components/flags/ad.js b/components/flags/ad.js deleted file mode 100644 index bfd6118a6..000000000 --- a/components/flags/ad.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagAD from 'flag-icons/flags/1x1/ad.svg' - -export const FlagAD = () => ( - -) -export default FlagAD diff --git a/components/flags/ae.js b/components/flags/ae.js deleted file mode 100644 index 6a0aa50f3..000000000 --- a/components/flags/ae.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagAE from 'flag-icons/flags/1x1/ae.svg' - -export const FlagAE = () => ( - -) -export default FlagAE diff --git a/components/flags/af.js b/components/flags/af.js deleted file mode 100644 index cc547ad63..000000000 --- a/components/flags/af.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagAF from 'flag-icons/flags/1x1/af.svg' - -export const FlagAF = () => ( - -) -export default FlagAF diff --git a/components/flags/ag.js b/components/flags/ag.js deleted file mode 100644 index 4476a3c88..000000000 --- a/components/flags/ag.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagAG from 'flag-icons/flags/1x1/ag.svg' - -export const FlagAG = () => ( - -) -export default FlagAG diff --git a/components/flags/ai.js b/components/flags/ai.js deleted file mode 100644 index af0237922..000000000 --- a/components/flags/ai.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagAI from 'flag-icons/flags/1x1/ai.svg' - -export const FlagAI = () => ( - -) -export default FlagAI diff --git a/components/flags/al.js b/components/flags/al.js deleted file mode 100644 index d5b66c000..000000000 --- a/components/flags/al.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagAL from 'flag-icons/flags/1x1/al.svg' - -export const FlagAL = () => ( - -) -export default FlagAL diff --git a/components/flags/am.js b/components/flags/am.js deleted file mode 100644 index b78eb8e7f..000000000 --- a/components/flags/am.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagAM from 'flag-icons/flags/1x1/am.svg' - -export const FlagAM = () => ( - -) -export default FlagAM diff --git a/components/flags/ao.js b/components/flags/ao.js deleted file mode 100644 index 72e4af788..000000000 --- a/components/flags/ao.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagAO from 'flag-icons/flags/1x1/ao.svg' - -export const FlagAO = () => ( - -) -export default FlagAO diff --git a/components/flags/aq.js b/components/flags/aq.js deleted file mode 100644 index eb71adb44..000000000 --- a/components/flags/aq.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagAQ from 'flag-icons/flags/1x1/aq.svg' - -export const FlagAQ = () => ( - -) -export default FlagAQ diff --git a/components/flags/ar.js b/components/flags/ar.js deleted file mode 100644 index b706faffc..000000000 --- a/components/flags/ar.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagAR from 'flag-icons/flags/1x1/ar.svg' - -export const FlagAR = () => ( - -) -export default FlagAR diff --git a/components/flags/as.js b/components/flags/as.js deleted file mode 100644 index b0ff1e9f3..000000000 --- a/components/flags/as.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagAS from 'flag-icons/flags/1x1/as.svg' - -export const FlagAS = () => ( - -) -export default FlagAS diff --git a/components/flags/at.js b/components/flags/at.js deleted file mode 100644 index 8402693f0..000000000 --- a/components/flags/at.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagAT from 'flag-icons/flags/1x1/at.svg' - -export const FlagAT = () => ( - -) -export default FlagAT diff --git a/components/flags/au.js b/components/flags/au.js deleted file mode 100644 index e56d74515..000000000 --- a/components/flags/au.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagAU from 'flag-icons/flags/1x1/au.svg' - -export const FlagAU = () => ( - -) -export default FlagAU diff --git a/components/flags/aw.js b/components/flags/aw.js deleted file mode 100644 index 998ae575f..000000000 --- a/components/flags/aw.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagAW from 'flag-icons/flags/1x1/aw.svg' - -export const FlagAW = () => ( - -) -export default FlagAW diff --git a/components/flags/ax.js b/components/flags/ax.js deleted file mode 100644 index 506ad9f61..000000000 --- a/components/flags/ax.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagAX from 'flag-icons/flags/1x1/ax.svg' - -export const FlagAX = () => ( - -) -export default FlagAX diff --git a/components/flags/az.js b/components/flags/az.js deleted file mode 100644 index 4a6c56cd1..000000000 --- a/components/flags/az.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagAZ from 'flag-icons/flags/1x1/az.svg' - -export const FlagAZ = () => ( - -) -export default FlagAZ diff --git a/components/flags/ba.js b/components/flags/ba.js deleted file mode 100644 index c92a18767..000000000 --- a/components/flags/ba.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagBA from 'flag-icons/flags/1x1/ba.svg' - -export const FlagBA = () => ( - -) -export default FlagBA diff --git a/components/flags/bb.js b/components/flags/bb.js deleted file mode 100644 index 5bda06275..000000000 --- a/components/flags/bb.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagBB from 'flag-icons/flags/1x1/bb.svg' - -export const FlagBB = () => ( - -) -export default FlagBB diff --git a/components/flags/bd.js b/components/flags/bd.js deleted file mode 100644 index 1faaa92ba..000000000 --- a/components/flags/bd.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagBD from 'flag-icons/flags/1x1/bd.svg' - -export const FlagBD = () => ( - -) -export default FlagBD diff --git a/components/flags/be.js b/components/flags/be.js deleted file mode 100644 index 628cd0bd1..000000000 --- a/components/flags/be.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagBE from 'flag-icons/flags/1x1/be.svg' - -export const FlagBE = () => ( - -) -export default FlagBE diff --git a/components/flags/bf.js b/components/flags/bf.js deleted file mode 100644 index f08cf5617..000000000 --- a/components/flags/bf.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagBF from 'flag-icons/flags/1x1/bf.svg' - -export const FlagBF = () => ( - -) -export default FlagBF diff --git a/components/flags/bg.js b/components/flags/bg.js deleted file mode 100644 index c3e2cfe0f..000000000 --- a/components/flags/bg.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagBG from 'flag-icons/flags/1x1/bg.svg' - -export const FlagBG = () => ( - -) -export default FlagBG diff --git a/components/flags/bh.js b/components/flags/bh.js deleted file mode 100644 index 52dc57df1..000000000 --- a/components/flags/bh.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagBH from 'flag-icons/flags/1x1/bh.svg' - -export const FlagBH = () => ( - -) -export default FlagBH diff --git a/components/flags/bi.js b/components/flags/bi.js deleted file mode 100644 index 2473d4a9b..000000000 --- a/components/flags/bi.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagBI from 'flag-icons/flags/1x1/bi.svg' - -export const FlagBI = () => ( - -) -export default FlagBI diff --git a/components/flags/bj.js b/components/flags/bj.js deleted file mode 100644 index 8d06af83c..000000000 --- a/components/flags/bj.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagBJ from 'flag-icons/flags/1x1/bj.svg' - -export const FlagBJ = () => ( - -) -export default FlagBJ diff --git a/components/flags/bl.js b/components/flags/bl.js deleted file mode 100644 index ed65d7806..000000000 --- a/components/flags/bl.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagBL from 'flag-icons/flags/1x1/bl.svg' - -export const FlagBL = () => ( - -) -export default FlagBL diff --git a/components/flags/bm.js b/components/flags/bm.js deleted file mode 100644 index 67a02c2ba..000000000 --- a/components/flags/bm.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagBM from 'flag-icons/flags/1x1/bm.svg' - -export const FlagBM = () => ( - -) -export default FlagBM diff --git a/components/flags/bn.js b/components/flags/bn.js deleted file mode 100644 index 3b6827077..000000000 --- a/components/flags/bn.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagBN from 'flag-icons/flags/1x1/bn.svg' - -export const FlagBN = () => ( - -) -export default FlagBN diff --git a/components/flags/bo.js b/components/flags/bo.js deleted file mode 100644 index 40739e0bc..000000000 --- a/components/flags/bo.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagBO from 'flag-icons/flags/1x1/bo.svg' - -export const FlagBO = () => ( - -) -export default FlagBO diff --git a/components/flags/bq.js b/components/flags/bq.js deleted file mode 100644 index 4455ecb02..000000000 --- a/components/flags/bq.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagBQ from 'flag-icons/flags/1x1/bq.svg' - -export const FlagBQ = () => ( - -) -export default FlagBQ diff --git a/components/flags/br.js b/components/flags/br.js deleted file mode 100644 index f7b4e5c54..000000000 --- a/components/flags/br.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagBR from 'flag-icons/flags/1x1/br.svg' - -export const FlagBR = () => ( - -) -export default FlagBR diff --git a/components/flags/bs.js b/components/flags/bs.js deleted file mode 100644 index badea1be5..000000000 --- a/components/flags/bs.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagBS from 'flag-icons/flags/1x1/bs.svg' - -export const FlagBS = () => ( - -) -export default FlagBS diff --git a/components/flags/bt.js b/components/flags/bt.js deleted file mode 100644 index 03067b58c..000000000 --- a/components/flags/bt.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagBT from 'flag-icons/flags/1x1/bt.svg' - -export const FlagBT = () => ( - -) -export default FlagBT diff --git a/components/flags/bv.js b/components/flags/bv.js deleted file mode 100644 index ead0d1763..000000000 --- a/components/flags/bv.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagBV from 'flag-icons/flags/1x1/bv.svg' - -export const FlagBV = () => ( - -) -export default FlagBV diff --git a/components/flags/bw.js b/components/flags/bw.js deleted file mode 100644 index da3e15204..000000000 --- a/components/flags/bw.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagBW from 'flag-icons/flags/1x1/bw.svg' - -export const FlagBW = () => ( - -) -export default FlagBW diff --git a/components/flags/by.js b/components/flags/by.js deleted file mode 100644 index 02ad39378..000000000 --- a/components/flags/by.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagBY from 'flag-icons/flags/1x1/by.svg' - -export const FlagBY = () => ( - -) -export default FlagBY diff --git a/components/flags/bz.js b/components/flags/bz.js deleted file mode 100644 index cb4c3cc22..000000000 --- a/components/flags/bz.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagBZ from 'flag-icons/flags/1x1/bz.svg' - -export const FlagBZ = () => ( - -) -export default FlagBZ diff --git a/components/flags/ca.js b/components/flags/ca.js deleted file mode 100644 index ec68a7b08..000000000 --- a/components/flags/ca.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagCA from 'flag-icons/flags/1x1/ca.svg' - -export const FlagCA = () => ( - -) -export default FlagCA diff --git a/components/flags/cc.js b/components/flags/cc.js deleted file mode 100644 index 265ffdf44..000000000 --- a/components/flags/cc.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagCC from 'flag-icons/flags/1x1/cc.svg' - -export const FlagCC = () => ( - -) -export default FlagCC diff --git a/components/flags/cd.js b/components/flags/cd.js deleted file mode 100644 index 99df3078b..000000000 --- a/components/flags/cd.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagCD from 'flag-icons/flags/1x1/cd.svg' - -export const FlagCD = () => ( - -) -export default FlagCD diff --git a/components/flags/cf.js b/components/flags/cf.js deleted file mode 100644 index 2948ef904..000000000 --- a/components/flags/cf.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagCF from 'flag-icons/flags/1x1/cf.svg' - -export const FlagCF = () => ( - -) -export default FlagCF diff --git a/components/flags/cg.js b/components/flags/cg.js deleted file mode 100644 index f213803bf..000000000 --- a/components/flags/cg.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagCG from 'flag-icons/flags/1x1/cg.svg' - -export const FlagCG = () => ( - -) -export default FlagCG diff --git a/components/flags/ch.js b/components/flags/ch.js deleted file mode 100644 index 861e87a4c..000000000 --- a/components/flags/ch.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagCH from 'flag-icons/flags/1x1/ch.svg' - -export const FlagCH = () => ( - -) -export default FlagCH diff --git a/components/flags/ci.js b/components/flags/ci.js deleted file mode 100644 index 302ea59be..000000000 --- a/components/flags/ci.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagCI from 'flag-icons/flags/1x1/ci.svg' - -export const FlagCI = () => ( - -) -export default FlagCI diff --git a/components/flags/ck.js b/components/flags/ck.js deleted file mode 100644 index 7ab77a969..000000000 --- a/components/flags/ck.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagCK from 'flag-icons/flags/1x1/ck.svg' - -export const FlagCK = () => ( - -) -export default FlagCK diff --git a/components/flags/cl.js b/components/flags/cl.js deleted file mode 100644 index e4038eb73..000000000 --- a/components/flags/cl.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagCL from 'flag-icons/flags/1x1/cl.svg' - -export const FlagCL = () => ( - -) -export default FlagCL diff --git a/components/flags/cm.js b/components/flags/cm.js deleted file mode 100644 index 9de6afdb3..000000000 --- a/components/flags/cm.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagCM from 'flag-icons/flags/1x1/cm.svg' - -export const FlagCM = () => ( - -) -export default FlagCM diff --git a/components/flags/cn.js b/components/flags/cn.js deleted file mode 100644 index 3291a5244..000000000 --- a/components/flags/cn.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagCN from 'flag-icons/flags/1x1/cn.svg' - -export const FlagCN = () => ( - -) -export default FlagCN diff --git a/components/flags/co.js b/components/flags/co.js deleted file mode 100644 index f7020506d..000000000 --- a/components/flags/co.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagCO from 'flag-icons/flags/1x1/co.svg' - -export const FlagCO = () => ( - -) -export default FlagCO diff --git a/components/flags/cr.js b/components/flags/cr.js deleted file mode 100644 index a9c96fd7e..000000000 --- a/components/flags/cr.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagCR from 'flag-icons/flags/1x1/cr.svg' - -export const FlagCR = () => ( - -) -export default FlagCR diff --git a/components/flags/cu.js b/components/flags/cu.js deleted file mode 100644 index 005b1d5e9..000000000 --- a/components/flags/cu.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagCU from 'flag-icons/flags/1x1/cu.svg' - -export const FlagCU = () => ( - -) -export default FlagCU diff --git a/components/flags/cv.js b/components/flags/cv.js deleted file mode 100644 index dca92b38e..000000000 --- a/components/flags/cv.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagCV from 'flag-icons/flags/1x1/cv.svg' - -export const FlagCV = () => ( - -) -export default FlagCV diff --git a/components/flags/cw.js b/components/flags/cw.js deleted file mode 100644 index 100e4879c..000000000 --- a/components/flags/cw.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagCW from 'flag-icons/flags/1x1/cw.svg' - -export const FlagCW = () => ( - -) -export default FlagCW diff --git a/components/flags/cx.js b/components/flags/cx.js deleted file mode 100644 index f6b67428c..000000000 --- a/components/flags/cx.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagCX from 'flag-icons/flags/1x1/cx.svg' - -export const FlagCX = () => ( - -) -export default FlagCX diff --git a/components/flags/cy.js b/components/flags/cy.js deleted file mode 100644 index 94e07c33b..000000000 --- a/components/flags/cy.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagCY from 'flag-icons/flags/1x1/cy.svg' - -export const FlagCY = () => ( - -) -export default FlagCY diff --git a/components/flags/cz.js b/components/flags/cz.js deleted file mode 100644 index ad17db110..000000000 --- a/components/flags/cz.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagCZ from 'flag-icons/flags/1x1/cz.svg' - -export const FlagCZ = () => ( - -) -export default FlagCZ diff --git a/components/flags/de.js b/components/flags/de.js deleted file mode 100644 index f5abe79a8..000000000 --- a/components/flags/de.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagDE from 'flag-icons/flags/1x1/de.svg' - -export const FlagDE = () => ( - -) -export default FlagDE diff --git a/components/flags/dj.js b/components/flags/dj.js deleted file mode 100644 index 307ebb5da..000000000 --- a/components/flags/dj.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagDJ from 'flag-icons/flags/1x1/dj.svg' - -export const FlagDJ = () => ( - -) -export default FlagDJ diff --git a/components/flags/dk.js b/components/flags/dk.js deleted file mode 100644 index f405a93ef..000000000 --- a/components/flags/dk.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagDK from 'flag-icons/flags/1x1/dk.svg' - -export const FlagDK = () => ( - -) -export default FlagDK diff --git a/components/flags/dm.js b/components/flags/dm.js deleted file mode 100644 index be459ad8d..000000000 --- a/components/flags/dm.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagDM from 'flag-icons/flags/1x1/dm.svg' - -export const FlagDM = () => ( - -) -export default FlagDM diff --git a/components/flags/do.js b/components/flags/do.js deleted file mode 100644 index a5eb2780e..000000000 --- a/components/flags/do.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagDO from 'flag-icons/flags/1x1/do.svg' - -export const FlagDO = () => ( - -) -export default FlagDO diff --git a/components/flags/dz.js b/components/flags/dz.js deleted file mode 100644 index e26b4a0c3..000000000 --- a/components/flags/dz.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagDZ from 'flag-icons/flags/1x1/dz.svg' - -export const FlagDZ = () => ( - -) -export default FlagDZ diff --git a/components/flags/ec.js b/components/flags/ec.js deleted file mode 100644 index 8518c7bc0..000000000 --- a/components/flags/ec.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagEC from 'flag-icons/flags/1x1/ec.svg' - -export const FlagEC = () => ( - -) -export default FlagEC diff --git a/components/flags/ee.js b/components/flags/ee.js deleted file mode 100644 index 86101a07a..000000000 --- a/components/flags/ee.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagEE from 'flag-icons/flags/1x1/ee.svg' - -export const FlagEE = () => ( - -) -export default FlagEE diff --git a/components/flags/eg.js b/components/flags/eg.js deleted file mode 100644 index 5d7062f3c..000000000 --- a/components/flags/eg.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagEG from 'flag-icons/flags/1x1/eg.svg' - -export const FlagEG = () => ( - -) -export default FlagEG diff --git a/components/flags/eh.js b/components/flags/eh.js deleted file mode 100644 index 6ee976e77..000000000 --- a/components/flags/eh.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagEH from 'flag-icons/flags/1x1/eh.svg' - -export const FlagEH = () => ( - -) -export default FlagEH diff --git a/components/flags/er.js b/components/flags/er.js deleted file mode 100644 index 2f94a3455..000000000 --- a/components/flags/er.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagER from 'flag-icons/flags/1x1/er.svg' - -export const FlagER = () => ( - -) -export default FlagER diff --git a/components/flags/es.js b/components/flags/es.js deleted file mode 100644 index 975f06e92..000000000 --- a/components/flags/es.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagES from 'flag-icons/flags/1x1/es.svg' - -export const FlagES = () => ( - -) -export default FlagES diff --git a/components/flags/et.js b/components/flags/et.js deleted file mode 100644 index 1c1ccd364..000000000 --- a/components/flags/et.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagET from 'flag-icons/flags/1x1/et.svg' - -export const FlagET = () => ( - -) -export default FlagET diff --git a/components/flags/eu.js b/components/flags/eu.js deleted file mode 100644 index ca8a24187..000000000 --- a/components/flags/eu.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagEU from 'flag-icons/flags/1x1/eu.svg' - -export const FlagEU = () => ( - -) -export default FlagEU diff --git a/components/flags/fi.js b/components/flags/fi.js deleted file mode 100644 index 8e854ee96..000000000 --- a/components/flags/fi.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagFI from 'flag-icons/flags/1x1/fi.svg' - -export const FlagFI = () => ( - -) -export default FlagFI diff --git a/components/flags/fj.js b/components/flags/fj.js deleted file mode 100644 index 76dded3e4..000000000 --- a/components/flags/fj.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagFJ from 'flag-icons/flags/1x1/fj.svg' - -export const FlagFJ = () => ( - -) -export default FlagFJ diff --git a/components/flags/fk.js b/components/flags/fk.js deleted file mode 100644 index 474fb72ae..000000000 --- a/components/flags/fk.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagFK from 'flag-icons/flags/1x1/fk.svg' - -export const FlagFK = () => ( - -) -export default FlagFK diff --git a/components/flags/fm.js b/components/flags/fm.js deleted file mode 100644 index 22a517af4..000000000 --- a/components/flags/fm.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagFM from 'flag-icons/flags/1x1/fm.svg' - -export const FlagFM = () => ( - -) -export default FlagFM diff --git a/components/flags/fo.js b/components/flags/fo.js deleted file mode 100644 index 5c34494b8..000000000 --- a/components/flags/fo.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagFO from 'flag-icons/flags/1x1/fo.svg' - -export const FlagFO = () => ( - -) -export default FlagFO diff --git a/components/flags/fr.js b/components/flags/fr.js deleted file mode 100644 index 8d2579fc5..000000000 --- a/components/flags/fr.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagFR from 'flag-icons/flags/1x1/fr.svg' - -export const FlagFR = () => ( - -) -export default FlagFR diff --git a/components/flags/ga.js b/components/flags/ga.js deleted file mode 100644 index e2afb9ca2..000000000 --- a/components/flags/ga.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagGA from 'flag-icons/flags/1x1/ga.svg' - -export const FlagGA = () => ( - -) -export default FlagGA diff --git a/components/flags/gb.js b/components/flags/gb.js deleted file mode 100644 index 007cca496..000000000 --- a/components/flags/gb.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagGB from 'flag-icons/flags/1x1/gb.svg' - -export const FlagGB = () => ( - -) -export default FlagGB diff --git a/components/flags/gd.js b/components/flags/gd.js deleted file mode 100644 index 35e79c523..000000000 --- a/components/flags/gd.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagGD from 'flag-icons/flags/1x1/gd.svg' - -export const FlagGD = () => ( - -) -export default FlagGD diff --git a/components/flags/ge.js b/components/flags/ge.js deleted file mode 100644 index 632a30ec3..000000000 --- a/components/flags/ge.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagGE from 'flag-icons/flags/1x1/ge.svg' - -export const FlagGE = () => ( - -) -export default FlagGE diff --git a/components/flags/gf.js b/components/flags/gf.js deleted file mode 100644 index 4ec9038d2..000000000 --- a/components/flags/gf.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagGF from 'flag-icons/flags/1x1/gf.svg' - -export const FlagGF = () => ( - -) -export default FlagGF diff --git a/components/flags/gg.js b/components/flags/gg.js deleted file mode 100644 index 807a686b8..000000000 --- a/components/flags/gg.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagGG from 'flag-icons/flags/1x1/gg.svg' - -export const FlagGG = () => ( - -) -export default FlagGG diff --git a/components/flags/gh.js b/components/flags/gh.js deleted file mode 100644 index a8e914805..000000000 --- a/components/flags/gh.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagGH from 'flag-icons/flags/1x1/gh.svg' - -export const FlagGH = () => ( - -) -export default FlagGH diff --git a/components/flags/gi.js b/components/flags/gi.js deleted file mode 100644 index d3fcd45b4..000000000 --- a/components/flags/gi.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagGI from 'flag-icons/flags/1x1/gi.svg' - -export const FlagGI = () => ( - -) -export default FlagGI diff --git a/components/flags/gl.js b/components/flags/gl.js deleted file mode 100644 index a9c29476c..000000000 --- a/components/flags/gl.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagGL from 'flag-icons/flags/1x1/gl.svg' - -export const FlagGL = () => ( - -) -export default FlagGL diff --git a/components/flags/gm.js b/components/flags/gm.js deleted file mode 100644 index 60d71dd0c..000000000 --- a/components/flags/gm.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagGM from 'flag-icons/flags/1x1/gm.svg' - -export const FlagGM = () => ( - -) -export default FlagGM diff --git a/components/flags/gn.js b/components/flags/gn.js deleted file mode 100644 index 17f1b335d..000000000 --- a/components/flags/gn.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagGN from 'flag-icons/flags/1x1/gn.svg' - -export const FlagGN = () => ( - -) -export default FlagGN diff --git a/components/flags/gp.js b/components/flags/gp.js deleted file mode 100644 index fb9c291c3..000000000 --- a/components/flags/gp.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagGP from 'flag-icons/flags/1x1/gp.svg' - -export const FlagGP = () => ( - -) -export default FlagGP diff --git a/components/flags/gq.js b/components/flags/gq.js deleted file mode 100644 index 601606531..000000000 --- a/components/flags/gq.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagGQ from 'flag-icons/flags/1x1/gq.svg' - -export const FlagGQ = () => ( - -) -export default FlagGQ diff --git a/components/flags/gr.js b/components/flags/gr.js deleted file mode 100644 index ea30b5298..000000000 --- a/components/flags/gr.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagGR from 'flag-icons/flags/1x1/gr.svg' - -export const FlagGR = () => ( - -) -export default FlagGR diff --git a/components/flags/gs.js b/components/flags/gs.js deleted file mode 100644 index d1b6ead78..000000000 --- a/components/flags/gs.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagGS from 'flag-icons/flags/1x1/gs.svg' - -export const FlagGS = () => ( - -) -export default FlagGS diff --git a/components/flags/gt.js b/components/flags/gt.js deleted file mode 100644 index 3b880c4d2..000000000 --- a/components/flags/gt.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagGT from 'flag-icons/flags/1x1/gt.svg' - -export const FlagGT = () => ( - -) -export default FlagGT diff --git a/components/flags/gu.js b/components/flags/gu.js deleted file mode 100644 index c658c9c84..000000000 --- a/components/flags/gu.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagGU from 'flag-icons/flags/1x1/gu.svg' - -export const FlagGU = () => ( - -) -export default FlagGU diff --git a/components/flags/gw.js b/components/flags/gw.js deleted file mode 100644 index 5176005f6..000000000 --- a/components/flags/gw.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagGW from 'flag-icons/flags/1x1/gw.svg' - -export const FlagGW = () => ( - -) -export default FlagGW diff --git a/components/flags/gy.js b/components/flags/gy.js deleted file mode 100644 index 7d13aa4e9..000000000 --- a/components/flags/gy.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagGY from 'flag-icons/flags/1x1/gy.svg' - -export const FlagGY = () => ( - -) -export default FlagGY diff --git a/components/flags/hk.js b/components/flags/hk.js deleted file mode 100644 index a80dbd954..000000000 --- a/components/flags/hk.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagHK from 'flag-icons/flags/1x1/hk.svg' - -export const FlagHK = () => ( - -) -export default FlagHK diff --git a/components/flags/hm.js b/components/flags/hm.js deleted file mode 100644 index e9b3b2d40..000000000 --- a/components/flags/hm.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagHM from 'flag-icons/flags/1x1/hm.svg' - -export const FlagHM = () => ( - -) -export default FlagHM diff --git a/components/flags/hn.js b/components/flags/hn.js deleted file mode 100644 index d07a176aa..000000000 --- a/components/flags/hn.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagHN from 'flag-icons/flags/1x1/hn.svg' - -export const FlagHN = () => ( - -) -export default FlagHN diff --git a/components/flags/hr.js b/components/flags/hr.js deleted file mode 100644 index 478aeda2e..000000000 --- a/components/flags/hr.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagHR from 'flag-icons/flags/1x1/hr.svg' - -export const FlagHR = () => ( - -) -export default FlagHR diff --git a/components/flags/ht.js b/components/flags/ht.js deleted file mode 100644 index c24358543..000000000 --- a/components/flags/ht.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagHT from 'flag-icons/flags/1x1/ht.svg' - -export const FlagHT = () => ( - -) -export default FlagHT diff --git a/components/flags/hu.js b/components/flags/hu.js deleted file mode 100644 index 5971295b6..000000000 --- a/components/flags/hu.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagHU from 'flag-icons/flags/1x1/hu.svg' - -export const FlagHU = () => ( - -) -export default FlagHU diff --git a/components/flags/id.js b/components/flags/id.js deleted file mode 100644 index 5aacae71a..000000000 --- a/components/flags/id.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagID from 'flag-icons/flags/1x1/id.svg' - -export const FlagID = () => ( - -) -export default FlagID diff --git a/components/flags/ie.js b/components/flags/ie.js deleted file mode 100644 index 302287a15..000000000 --- a/components/flags/ie.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagIE from 'flag-icons/flags/1x1/ie.svg' - -export const FlagIE = () => ( - -) -export default FlagIE diff --git a/components/flags/il.js b/components/flags/il.js deleted file mode 100644 index 3a0142617..000000000 --- a/components/flags/il.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagIL from 'flag-icons/flags/1x1/il.svg' - -export const FlagIL = () => ( - -) -export default FlagIL diff --git a/components/flags/im.js b/components/flags/im.js deleted file mode 100644 index 87b527ca6..000000000 --- a/components/flags/im.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagIM from 'flag-icons/flags/1x1/im.svg' - -export const FlagIM = () => ( - -) -export default FlagIM diff --git a/components/flags/in.js b/components/flags/in.js deleted file mode 100644 index de1620295..000000000 --- a/components/flags/in.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagIN from 'flag-icons/flags/1x1/in.svg' - -export const FlagIN = () => ( - -) -export default FlagIN diff --git a/components/flags/io.js b/components/flags/io.js deleted file mode 100644 index 73aa2cb5e..000000000 --- a/components/flags/io.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagIO from 'flag-icons/flags/1x1/io.svg' - -export const FlagIO = () => ( - -) -export default FlagIO diff --git a/components/flags/iq.js b/components/flags/iq.js deleted file mode 100644 index 04c1b36a6..000000000 --- a/components/flags/iq.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagIQ from 'flag-icons/flags/1x1/iq.svg' - -export const FlagIQ = () => ( - -) -export default FlagIQ diff --git a/components/flags/ir.js b/components/flags/ir.js deleted file mode 100644 index 24072457e..000000000 --- a/components/flags/ir.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagIR from 'flag-icons/flags/1x1/ir.svg' - -export const FlagIR = () => ( - -) -export default FlagIR diff --git a/components/flags/is.js b/components/flags/is.js deleted file mode 100644 index 407309e26..000000000 --- a/components/flags/is.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagIS from 'flag-icons/flags/1x1/is.svg' - -export const FlagIS = () => ( - -) -export default FlagIS diff --git a/components/flags/it.js b/components/flags/it.js deleted file mode 100644 index 55243995c..000000000 --- a/components/flags/it.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagIT from 'flag-icons/flags/1x1/it.svg' - -export const FlagIT = () => ( - -) -export default FlagIT diff --git a/components/flags/je.js b/components/flags/je.js deleted file mode 100644 index ec22c5b28..000000000 --- a/components/flags/je.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagJE from 'flag-icons/flags/1x1/je.svg' - -export const FlagJE = () => ( - -) -export default FlagJE diff --git a/components/flags/jm.js b/components/flags/jm.js deleted file mode 100644 index f9f135c64..000000000 --- a/components/flags/jm.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagJM from 'flag-icons/flags/1x1/jm.svg' - -export const FlagJM = () => ( - -) -export default FlagJM diff --git a/components/flags/jo.js b/components/flags/jo.js deleted file mode 100644 index 514ec4b2a..000000000 --- a/components/flags/jo.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagJO from 'flag-icons/flags/1x1/jo.svg' - -export const FlagJO = () => ( - -) -export default FlagJO diff --git a/components/flags/jp.js b/components/flags/jp.js deleted file mode 100644 index 274e605a2..000000000 --- a/components/flags/jp.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagJP from 'flag-icons/flags/1x1/jp.svg' - -export const FlagJP = () => ( - -) -export default FlagJP diff --git a/components/flags/ke.js b/components/flags/ke.js deleted file mode 100644 index 08698b51e..000000000 --- a/components/flags/ke.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagKE from 'flag-icons/flags/1x1/ke.svg' - -export const FlagKE = () => ( - -) -export default FlagKE diff --git a/components/flags/kg.js b/components/flags/kg.js deleted file mode 100644 index 8e3bd2fcd..000000000 --- a/components/flags/kg.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagKG from 'flag-icons/flags/1x1/kg.svg' - -export const FlagKG = () => ( - -) -export default FlagKG diff --git a/components/flags/kh.js b/components/flags/kh.js deleted file mode 100644 index af0fba571..000000000 --- a/components/flags/kh.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagKH from 'flag-icons/flags/1x1/kh.svg' - -export const FlagKH = () => ( - -) -export default FlagKH diff --git a/components/flags/ki.js b/components/flags/ki.js deleted file mode 100644 index d156dadc5..000000000 --- a/components/flags/ki.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagKI from 'flag-icons/flags/1x1/ki.svg' - -export const FlagKI = () => ( - -) -export default FlagKI diff --git a/components/flags/km.js b/components/flags/km.js deleted file mode 100644 index c62c0c040..000000000 --- a/components/flags/km.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagKM from 'flag-icons/flags/1x1/km.svg' - -export const FlagKM = () => ( - -) -export default FlagKM diff --git a/components/flags/kn.js b/components/flags/kn.js deleted file mode 100644 index eecbe107b..000000000 --- a/components/flags/kn.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagKN from 'flag-icons/flags/1x1/kn.svg' - -export const FlagKN = () => ( - -) -export default FlagKN diff --git a/components/flags/kp.js b/components/flags/kp.js deleted file mode 100644 index 2c6ece65a..000000000 --- a/components/flags/kp.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagKP from 'flag-icons/flags/1x1/kp.svg' - -export const FlagKP = () => ( - -) -export default FlagKP diff --git a/components/flags/kr.js b/components/flags/kr.js deleted file mode 100644 index 5e2d70d14..000000000 --- a/components/flags/kr.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagKR from 'flag-icons/flags/1x1/kr.svg' - -export const FlagKR = () => ( - -) -export default FlagKR diff --git a/components/flags/kw.js b/components/flags/kw.js deleted file mode 100644 index 325522b40..000000000 --- a/components/flags/kw.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagKW from 'flag-icons/flags/1x1/kw.svg' - -export const FlagKW = () => ( - -) -export default FlagKW diff --git a/components/flags/ky.js b/components/flags/ky.js deleted file mode 100644 index 80f6c2683..000000000 --- a/components/flags/ky.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagKY from 'flag-icons/flags/1x1/ky.svg' - -export const FlagKY = () => ( - -) -export default FlagKY diff --git a/components/flags/kz.js b/components/flags/kz.js deleted file mode 100644 index 10254c2f4..000000000 --- a/components/flags/kz.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagKZ from 'flag-icons/flags/1x1/kz.svg' - -export const FlagKZ = () => ( - -) -export default FlagKZ diff --git a/components/flags/la.js b/components/flags/la.js deleted file mode 100644 index e066f5a44..000000000 --- a/components/flags/la.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagLA from 'flag-icons/flags/1x1/la.svg' - -export const FlagLA = () => ( - -) -export default FlagLA diff --git a/components/flags/lb.js b/components/flags/lb.js deleted file mode 100644 index fd07b81e5..000000000 --- a/components/flags/lb.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagLB from 'flag-icons/flags/1x1/lb.svg' - -export const FlagLB = () => ( - -) -export default FlagLB diff --git a/components/flags/lc.js b/components/flags/lc.js deleted file mode 100644 index 0971477d3..000000000 --- a/components/flags/lc.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagLC from 'flag-icons/flags/1x1/lc.svg' - -export const FlagLC = () => ( - -) -export default FlagLC diff --git a/components/flags/li.js b/components/flags/li.js deleted file mode 100644 index a2bd3b08b..000000000 --- a/components/flags/li.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagLI from 'flag-icons/flags/1x1/li.svg' - -export const FlagLI = () => ( - -) -export default FlagLI diff --git a/components/flags/lk.js b/components/flags/lk.js deleted file mode 100644 index 62d921b69..000000000 --- a/components/flags/lk.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagLK from 'flag-icons/flags/1x1/lk.svg' - -export const FlagLK = () => ( - -) -export default FlagLK diff --git a/components/flags/lr.js b/components/flags/lr.js deleted file mode 100644 index 322b4a18e..000000000 --- a/components/flags/lr.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagLR from 'flag-icons/flags/1x1/lr.svg' - -export const FlagLR = () => ( - -) -export default FlagLR diff --git a/components/flags/ls.js b/components/flags/ls.js deleted file mode 100644 index c87482dd3..000000000 --- a/components/flags/ls.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagLS from 'flag-icons/flags/1x1/ls.svg' - -export const FlagLS = () => ( - -) -export default FlagLS diff --git a/components/flags/lt.js b/components/flags/lt.js deleted file mode 100644 index 09d042e82..000000000 --- a/components/flags/lt.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagLT from 'flag-icons/flags/1x1/lt.svg' - -export const FlagLT = () => ( - -) -export default FlagLT diff --git a/components/flags/lu.js b/components/flags/lu.js deleted file mode 100644 index 9f797f8f8..000000000 --- a/components/flags/lu.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagLU from 'flag-icons/flags/1x1/lu.svg' - -export const FlagLU = () => ( - -) -export default FlagLU diff --git a/components/flags/lv.js b/components/flags/lv.js deleted file mode 100644 index 1cb2307a6..000000000 --- a/components/flags/lv.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagLV from 'flag-icons/flags/1x1/lv.svg' - -export const FlagLV = () => ( - -) -export default FlagLV diff --git a/components/flags/ly.js b/components/flags/ly.js deleted file mode 100644 index e5c511154..000000000 --- a/components/flags/ly.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagLY from 'flag-icons/flags/1x1/ly.svg' - -export const FlagLY = () => ( - -) -export default FlagLY diff --git a/components/flags/ma.js b/components/flags/ma.js deleted file mode 100644 index 796a96f84..000000000 --- a/components/flags/ma.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagMA from 'flag-icons/flags/1x1/ma.svg' - -export const FlagMA = () => ( - -) -export default FlagMA diff --git a/components/flags/mc.js b/components/flags/mc.js deleted file mode 100644 index 930c369e2..000000000 --- a/components/flags/mc.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagMC from 'flag-icons/flags/1x1/mc.svg' - -export const FlagMC = () => ( - -) -export default FlagMC diff --git a/components/flags/md.js b/components/flags/md.js deleted file mode 100644 index 972ce9721..000000000 --- a/components/flags/md.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagMD from 'flag-icons/flags/1x1/md.svg' - -export const FlagMD = () => ( - -) -export default FlagMD diff --git a/components/flags/me.js b/components/flags/me.js deleted file mode 100644 index 83579f5d8..000000000 --- a/components/flags/me.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagME from 'flag-icons/flags/1x1/me.svg' - -export const FlagME = () => ( - -) -export default FlagME diff --git a/components/flags/mf.js b/components/flags/mf.js deleted file mode 100644 index 70f795ce5..000000000 --- a/components/flags/mf.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagMF from 'flag-icons/flags/1x1/mf.svg' - -export const FlagMF = () => ( - -) -export default FlagMF diff --git a/components/flags/mg.js b/components/flags/mg.js deleted file mode 100644 index e80c2dafd..000000000 --- a/components/flags/mg.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagMG from 'flag-icons/flags/1x1/mg.svg' - -export const FlagMG = () => ( - -) -export default FlagMG diff --git a/components/flags/mh.js b/components/flags/mh.js deleted file mode 100644 index 2d4b9fb98..000000000 --- a/components/flags/mh.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagMH from 'flag-icons/flags/1x1/mh.svg' - -export const FlagMH = () => ( - -) -export default FlagMH diff --git a/components/flags/mk.js b/components/flags/mk.js deleted file mode 100644 index fa4b757e5..000000000 --- a/components/flags/mk.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagMK from 'flag-icons/flags/1x1/mk.svg' - -export const FlagMK = () => ( - -) -export default FlagMK diff --git a/components/flags/ml.js b/components/flags/ml.js deleted file mode 100644 index 3df5d3f73..000000000 --- a/components/flags/ml.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagML from 'flag-icons/flags/1x1/ml.svg' - -export const FlagML = () => ( - -) -export default FlagML diff --git a/components/flags/mm.js b/components/flags/mm.js deleted file mode 100644 index 1450031c4..000000000 --- a/components/flags/mm.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagMM from 'flag-icons/flags/1x1/mm.svg' - -export const FlagMM = () => ( - -) -export default FlagMM diff --git a/components/flags/mn.js b/components/flags/mn.js deleted file mode 100644 index 26a9d82c9..000000000 --- a/components/flags/mn.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagMN from 'flag-icons/flags/1x1/mn.svg' - -export const FlagMN = () => ( - -) -export default FlagMN diff --git a/components/flags/mo.js b/components/flags/mo.js deleted file mode 100644 index 0f15078fe..000000000 --- a/components/flags/mo.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagMO from 'flag-icons/flags/1x1/mo.svg' - -export const FlagMO = () => ( - -) -export default FlagMO diff --git a/components/flags/mp.js b/components/flags/mp.js deleted file mode 100644 index d8b812e3e..000000000 --- a/components/flags/mp.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagMP from 'flag-icons/flags/1x1/mp.svg' - -export const FlagMP = () => ( - -) -export default FlagMP diff --git a/components/flags/mq.js b/components/flags/mq.js deleted file mode 100644 index 94e9682ec..000000000 --- a/components/flags/mq.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagMQ from 'flag-icons/flags/1x1/mq.svg' - -export const FlagMQ = () => ( - -) -export default FlagMQ diff --git a/components/flags/mr.js b/components/flags/mr.js deleted file mode 100644 index c51b49a29..000000000 --- a/components/flags/mr.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagMR from 'flag-icons/flags/1x1/mr.svg' - -export const FlagMR = () => ( - -) -export default FlagMR diff --git a/components/flags/ms.js b/components/flags/ms.js deleted file mode 100644 index d19b943d4..000000000 --- a/components/flags/ms.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagMS from 'flag-icons/flags/1x1/ms.svg' - -export const FlagMS = () => ( - -) -export default FlagMS diff --git a/components/flags/mt.js b/components/flags/mt.js deleted file mode 100644 index 4cdf9e3f9..000000000 --- a/components/flags/mt.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagMT from 'flag-icons/flags/1x1/mt.svg' - -export const FlagMT = () => ( - -) -export default FlagMT diff --git a/components/flags/mu.js b/components/flags/mu.js deleted file mode 100644 index b160f410f..000000000 --- a/components/flags/mu.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagMU from 'flag-icons/flags/1x1/mu.svg' - -export const FlagMU = () => ( - -) -export default FlagMU diff --git a/components/flags/mv.js b/components/flags/mv.js deleted file mode 100644 index d3eca67e5..000000000 --- a/components/flags/mv.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagMV from 'flag-icons/flags/1x1/mv.svg' - -export const FlagMV = () => ( - -) -export default FlagMV diff --git a/components/flags/mw.js b/components/flags/mw.js deleted file mode 100644 index 6c96afa1d..000000000 --- a/components/flags/mw.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagMW from 'flag-icons/flags/1x1/mw.svg' - -export const FlagMW = () => ( - -) -export default FlagMW diff --git a/components/flags/mx.js b/components/flags/mx.js deleted file mode 100644 index 98392d4e2..000000000 --- a/components/flags/mx.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagMX from 'flag-icons/flags/1x1/mx.svg' - -export const FlagMX = () => ( - -) -export default FlagMX diff --git a/components/flags/my.js b/components/flags/my.js deleted file mode 100644 index f0e575296..000000000 --- a/components/flags/my.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagMY from 'flag-icons/flags/1x1/my.svg' - -export const FlagMY = () => ( - -) -export default FlagMY diff --git a/components/flags/mz.js b/components/flags/mz.js deleted file mode 100644 index 2dcf0e474..000000000 --- a/components/flags/mz.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagMZ from 'flag-icons/flags/1x1/mz.svg' - -export const FlagMZ = () => ( - -) -export default FlagMZ diff --git a/components/flags/na.js b/components/flags/na.js deleted file mode 100644 index 9d53efaff..000000000 --- a/components/flags/na.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagNA from 'flag-icons/flags/1x1/na.svg' - -export const FlagNA = () => ( - -) -export default FlagNA diff --git a/components/flags/nc.js b/components/flags/nc.js deleted file mode 100644 index 01c026951..000000000 --- a/components/flags/nc.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagNC from 'flag-icons/flags/1x1/nc.svg' - -export const FlagNC = () => ( - -) -export default FlagNC diff --git a/components/flags/ne.js b/components/flags/ne.js deleted file mode 100644 index 9cecbd455..000000000 --- a/components/flags/ne.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagNE from 'flag-icons/flags/1x1/ne.svg' - -export const FlagNE = () => ( - -) -export default FlagNE diff --git a/components/flags/nf.js b/components/flags/nf.js deleted file mode 100644 index 30838b880..000000000 --- a/components/flags/nf.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagNF from 'flag-icons/flags/1x1/nf.svg' - -export const FlagNF = () => ( - -) -export default FlagNF diff --git a/components/flags/ng.js b/components/flags/ng.js deleted file mode 100644 index fd14443e9..000000000 --- a/components/flags/ng.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagNG from 'flag-icons/flags/1x1/ng.svg' - -export const FlagNG = () => ( - -) -export default FlagNG diff --git a/components/flags/ni.js b/components/flags/ni.js deleted file mode 100644 index 91a9adc1d..000000000 --- a/components/flags/ni.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagNI from 'flag-icons/flags/1x1/ni.svg' - -export const FlagNI = () => ( - -) -export default FlagNI diff --git a/components/flags/nl.js b/components/flags/nl.js deleted file mode 100644 index 282587afc..000000000 --- a/components/flags/nl.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagNL from 'flag-icons/flags/1x1/nl.svg' - -export const FlagNL = () => ( - -) -export default FlagNL diff --git a/components/flags/no.js b/components/flags/no.js deleted file mode 100644 index a2db10c0f..000000000 --- a/components/flags/no.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagNO from 'flag-icons/flags/1x1/no.svg' - -export const FlagNO = () => ( - -) -export default FlagNO diff --git a/components/flags/np.js b/components/flags/np.js deleted file mode 100644 index 419dd3018..000000000 --- a/components/flags/np.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagNP from 'flag-icons/flags/1x1/np.svg' - -export const FlagNP = () => ( - -) -export default FlagNP diff --git a/components/flags/nr.js b/components/flags/nr.js deleted file mode 100644 index ad205188e..000000000 --- a/components/flags/nr.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagNR from 'flag-icons/flags/1x1/nr.svg' - -export const FlagNR = () => ( - -) -export default FlagNR diff --git a/components/flags/nu.js b/components/flags/nu.js deleted file mode 100644 index 5637906a3..000000000 --- a/components/flags/nu.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagNU from 'flag-icons/flags/1x1/nu.svg' - -export const FlagNU = () => ( - -) -export default FlagNU diff --git a/components/flags/nz.js b/components/flags/nz.js deleted file mode 100644 index 3d4a0fc52..000000000 --- a/components/flags/nz.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagNZ from 'flag-icons/flags/1x1/nz.svg' - -export const FlagNZ = () => ( - -) -export default FlagNZ diff --git a/components/flags/om.js b/components/flags/om.js deleted file mode 100644 index 883dc6599..000000000 --- a/components/flags/om.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagOM from 'flag-icons/flags/1x1/om.svg' - -export const FlagOM = () => ( - -) -export default FlagOM diff --git a/components/flags/pa.js b/components/flags/pa.js deleted file mode 100644 index 3a86ed48a..000000000 --- a/components/flags/pa.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagPA from 'flag-icons/flags/1x1/pa.svg' - -export const FlagPA = () => ( - -) -export default FlagPA diff --git a/components/flags/pe.js b/components/flags/pe.js deleted file mode 100644 index 2c25b52b2..000000000 --- a/components/flags/pe.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagPE from 'flag-icons/flags/1x1/pe.svg' - -export const FlagPE = () => ( - -) -export default FlagPE diff --git a/components/flags/pf.js b/components/flags/pf.js deleted file mode 100644 index 255ae20ab..000000000 --- a/components/flags/pf.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagPF from 'flag-icons/flags/1x1/pf.svg' - -export const FlagPF = () => ( - -) -export default FlagPF diff --git a/components/flags/pg.js b/components/flags/pg.js deleted file mode 100644 index 3fe4a45fd..000000000 --- a/components/flags/pg.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagPG from 'flag-icons/flags/1x1/pg.svg' - -export const FlagPG = () => ( - -) -export default FlagPG diff --git a/components/flags/ph.js b/components/flags/ph.js deleted file mode 100644 index 86ea29925..000000000 --- a/components/flags/ph.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagPH from 'flag-icons/flags/1x1/ph.svg' - -export const FlagPH = () => ( - -) -export default FlagPH diff --git a/components/flags/pk.js b/components/flags/pk.js deleted file mode 100644 index af4dd3869..000000000 --- a/components/flags/pk.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagPK from 'flag-icons/flags/1x1/pk.svg' - -export const FlagPK = () => ( - -) -export default FlagPK diff --git a/components/flags/pl.js b/components/flags/pl.js deleted file mode 100644 index 7773f0c20..000000000 --- a/components/flags/pl.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagPL from 'flag-icons/flags/1x1/pl.svg' - -export const FlagPL = () => ( - -) -export default FlagPL diff --git a/components/flags/pm.js b/components/flags/pm.js deleted file mode 100644 index 1b96e3243..000000000 --- a/components/flags/pm.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagPM from 'flag-icons/flags/1x1/pm.svg' - -export const FlagPM = () => ( - -) -export default FlagPM diff --git a/components/flags/pn.js b/components/flags/pn.js deleted file mode 100644 index bfa17c05a..000000000 --- a/components/flags/pn.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagPN from 'flag-icons/flags/1x1/pn.svg' - -export const FlagPN = () => ( - -) -export default FlagPN diff --git a/components/flags/pr.js b/components/flags/pr.js deleted file mode 100644 index 4b770f25a..000000000 --- a/components/flags/pr.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagPR from 'flag-icons/flags/1x1/pr.svg' - -export const FlagPR = () => ( - -) -export default FlagPR diff --git a/components/flags/ps.js b/components/flags/ps.js deleted file mode 100644 index 14356e6f2..000000000 --- a/components/flags/ps.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagPS from 'flag-icons/flags/1x1/ps.svg' - -export const FlagPS = () => ( - -) -export default FlagPS diff --git a/components/flags/pt.js b/components/flags/pt.js deleted file mode 100644 index 90b7a5f72..000000000 --- a/components/flags/pt.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagPT from 'flag-icons/flags/1x1/pt.svg' - -export const FlagPT = () => ( - -) -export default FlagPT diff --git a/components/flags/pw.js b/components/flags/pw.js deleted file mode 100644 index ffcbed2f8..000000000 --- a/components/flags/pw.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagPW from 'flag-icons/flags/1x1/pw.svg' - -export const FlagPW = () => ( - -) -export default FlagPW diff --git a/components/flags/py.js b/components/flags/py.js deleted file mode 100644 index 5e997a5bc..000000000 --- a/components/flags/py.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagPY from 'flag-icons/flags/1x1/py.svg' - -export const FlagPY = () => ( - -) -export default FlagPY diff --git a/components/flags/qa.js b/components/flags/qa.js deleted file mode 100644 index eaada4546..000000000 --- a/components/flags/qa.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagQA from 'flag-icons/flags/1x1/qa.svg' - -export const FlagQA = () => ( - -) -export default FlagQA diff --git a/components/flags/re.js b/components/flags/re.js deleted file mode 100644 index 96421c15d..000000000 --- a/components/flags/re.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagRE from 'flag-icons/flags/1x1/re.svg' - -export const FlagRE = () => ( - -) -export default FlagRE diff --git a/components/flags/ro.js b/components/flags/ro.js deleted file mode 100644 index b865e0f2e..000000000 --- a/components/flags/ro.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagRO from 'flag-icons/flags/1x1/ro.svg' - -export const FlagRO = () => ( - -) -export default FlagRO diff --git a/components/flags/rs.js b/components/flags/rs.js deleted file mode 100644 index 2d6880114..000000000 --- a/components/flags/rs.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagRS from 'flag-icons/flags/1x1/rs.svg' - -export const FlagRS = () => ( - -) -export default FlagRS diff --git a/components/flags/ru.js b/components/flags/ru.js deleted file mode 100644 index 8686193e6..000000000 --- a/components/flags/ru.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagRU from 'flag-icons/flags/1x1/ru.svg' - -export const FlagRU = () => ( - -) -export default FlagRU diff --git a/components/flags/rw.js b/components/flags/rw.js deleted file mode 100644 index d02eb120a..000000000 --- a/components/flags/rw.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagRW from 'flag-icons/flags/1x1/rw.svg' - -export const FlagRW = () => ( - -) -export default FlagRW diff --git a/components/flags/sa.js b/components/flags/sa.js deleted file mode 100644 index 43ef455ec..000000000 --- a/components/flags/sa.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagSA from 'flag-icons/flags/1x1/sa.svg' - -export const FlagSA = () => ( - -) -export default FlagSA diff --git a/components/flags/sb.js b/components/flags/sb.js deleted file mode 100644 index 0bd1f7948..000000000 --- a/components/flags/sb.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagSB from 'flag-icons/flags/1x1/sb.svg' - -export const FlagSB = () => ( - -) -export default FlagSB diff --git a/components/flags/sc.js b/components/flags/sc.js deleted file mode 100644 index 9f5b8a0b2..000000000 --- a/components/flags/sc.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagSC from 'flag-icons/flags/1x1/sc.svg' - -export const FlagSC = () => ( - -) -export default FlagSC diff --git a/components/flags/sd.js b/components/flags/sd.js deleted file mode 100644 index 6807f8fbd..000000000 --- a/components/flags/sd.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagSD from 'flag-icons/flags/1x1/sd.svg' - -export const FlagSD = () => ( - -) -export default FlagSD diff --git a/components/flags/se.js b/components/flags/se.js deleted file mode 100644 index 7cefd8bbb..000000000 --- a/components/flags/se.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagSE from 'flag-icons/flags/1x1/se.svg' - -export const FlagSE = () => ( - -) -export default FlagSE diff --git a/components/flags/sg.js b/components/flags/sg.js deleted file mode 100644 index f5fed5a63..000000000 --- a/components/flags/sg.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagSG from 'flag-icons/flags/1x1/sg.svg' - -export const FlagSG = () => ( - -) -export default FlagSG diff --git a/components/flags/sh.js b/components/flags/sh.js deleted file mode 100644 index 3e2c4d8f3..000000000 --- a/components/flags/sh.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagSH from 'flag-icons/flags/1x1/sh.svg' - -export const FlagSH = () => ( - -) -export default FlagSH diff --git a/components/flags/si.js b/components/flags/si.js deleted file mode 100644 index 2f5a8fac2..000000000 --- a/components/flags/si.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagSI from 'flag-icons/flags/1x1/si.svg' - -export const FlagSI = () => ( - -) -export default FlagSI diff --git a/components/flags/sj.js b/components/flags/sj.js deleted file mode 100644 index dd8340152..000000000 --- a/components/flags/sj.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagSJ from 'flag-icons/flags/1x1/sj.svg' - -export const FlagSJ = () => ( - -) -export default FlagSJ diff --git a/components/flags/sk.js b/components/flags/sk.js deleted file mode 100644 index 79ea85ac5..000000000 --- a/components/flags/sk.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagSK from 'flag-icons/flags/1x1/sk.svg' - -export const FlagSK = () => ( - -) -export default FlagSK diff --git a/components/flags/sl.js b/components/flags/sl.js deleted file mode 100644 index 0ec1e0d34..000000000 --- a/components/flags/sl.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagSL from 'flag-icons/flags/1x1/sl.svg' - -export const FlagSL = () => ( - -) -export default FlagSL diff --git a/components/flags/sm.js b/components/flags/sm.js deleted file mode 100644 index df9735341..000000000 --- a/components/flags/sm.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagSM from 'flag-icons/flags/1x1/sm.svg' - -export const FlagSM = () => ( - -) -export default FlagSM diff --git a/components/flags/sn.js b/components/flags/sn.js deleted file mode 100644 index 552b35a74..000000000 --- a/components/flags/sn.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagSN from 'flag-icons/flags/1x1/sn.svg' - -export const FlagSN = () => ( - -) -export default FlagSN diff --git a/components/flags/so.js b/components/flags/so.js deleted file mode 100644 index 9522e3850..000000000 --- a/components/flags/so.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagSO from 'flag-icons/flags/1x1/so.svg' - -export const FlagSO = () => ( - -) -export default FlagSO diff --git a/components/flags/sr.js b/components/flags/sr.js deleted file mode 100644 index cd581b616..000000000 --- a/components/flags/sr.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagSR from 'flag-icons/flags/1x1/sr.svg' - -export const FlagSR = () => ( - -) -export default FlagSR diff --git a/components/flags/ss.js b/components/flags/ss.js deleted file mode 100644 index 22dede952..000000000 --- a/components/flags/ss.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagSS from 'flag-icons/flags/1x1/ss.svg' - -export const FlagSS = () => ( - -) -export default FlagSS diff --git a/components/flags/st.js b/components/flags/st.js deleted file mode 100644 index 0f5472371..000000000 --- a/components/flags/st.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagST from 'flag-icons/flags/1x1/st.svg' - -export const FlagST = () => ( - -) -export default FlagST diff --git a/components/flags/sv.js b/components/flags/sv.js deleted file mode 100644 index 655b62c4b..000000000 --- a/components/flags/sv.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagSV from 'flag-icons/flags/1x1/sv.svg' - -export const FlagSV = () => ( - -) -export default FlagSV diff --git a/components/flags/sx.js b/components/flags/sx.js deleted file mode 100644 index 48ff8c8b6..000000000 --- a/components/flags/sx.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagSX from 'flag-icons/flags/1x1/sx.svg' - -export const FlagSX = () => ( - -) -export default FlagSX diff --git a/components/flags/sy.js b/components/flags/sy.js deleted file mode 100644 index 9b7777245..000000000 --- a/components/flags/sy.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagSY from 'flag-icons/flags/1x1/sy.svg' - -export const FlagSY = () => ( - -) -export default FlagSY diff --git a/components/flags/sz.js b/components/flags/sz.js deleted file mode 100644 index cdf4cf073..000000000 --- a/components/flags/sz.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagSZ from 'flag-icons/flags/1x1/sz.svg' - -export const FlagSZ = () => ( - -) -export default FlagSZ diff --git a/components/flags/tc.js b/components/flags/tc.js deleted file mode 100644 index ee8278a14..000000000 --- a/components/flags/tc.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagTC from 'flag-icons/flags/1x1/tc.svg' - -export const FlagTC = () => ( - -) -export default FlagTC diff --git a/components/flags/td.js b/components/flags/td.js deleted file mode 100644 index cb89462e1..000000000 --- a/components/flags/td.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagTD from 'flag-icons/flags/1x1/td.svg' - -export const FlagTD = () => ( - -) -export default FlagTD diff --git a/components/flags/tf.js b/components/flags/tf.js deleted file mode 100644 index dd70ea1ed..000000000 --- a/components/flags/tf.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagTF from 'flag-icons/flags/1x1/tf.svg' - -export const FlagTF = () => ( - -) -export default FlagTF diff --git a/components/flags/tg.js b/components/flags/tg.js deleted file mode 100644 index 0eb701bcc..000000000 --- a/components/flags/tg.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagTG from 'flag-icons/flags/1x1/tg.svg' - -export const FlagTG = () => ( - -) -export default FlagTG diff --git a/components/flags/th.js b/components/flags/th.js deleted file mode 100644 index 2eb7b0d2d..000000000 --- a/components/flags/th.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagTH from 'flag-icons/flags/1x1/th.svg' - -export const FlagTH = () => ( - -) -export default FlagTH diff --git a/components/flags/tj.js b/components/flags/tj.js deleted file mode 100644 index 8d9d86360..000000000 --- a/components/flags/tj.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagTJ from 'flag-icons/flags/1x1/tj.svg' - -export const FlagTJ = () => ( - -) -export default FlagTJ diff --git a/components/flags/tk.js b/components/flags/tk.js deleted file mode 100644 index 6c5a0b908..000000000 --- a/components/flags/tk.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagTK from 'flag-icons/flags/1x1/tk.svg' - -export const FlagTK = () => ( - -) -export default FlagTK diff --git a/components/flags/tl.js b/components/flags/tl.js deleted file mode 100644 index f80d2a237..000000000 --- a/components/flags/tl.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagTL from 'flag-icons/flags/1x1/tl.svg' - -export const FlagTL = () => ( - -) -export default FlagTL diff --git a/components/flags/tm.js b/components/flags/tm.js deleted file mode 100644 index 037435123..000000000 --- a/components/flags/tm.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagTM from 'flag-icons/flags/1x1/tm.svg' - -export const FlagTM = () => ( - -) -export default FlagTM diff --git a/components/flags/tn.js b/components/flags/tn.js deleted file mode 100644 index e2ce81a19..000000000 --- a/components/flags/tn.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagTN from 'flag-icons/flags/1x1/tn.svg' - -export const FlagTN = () => ( - -) -export default FlagTN diff --git a/components/flags/to.js b/components/flags/to.js deleted file mode 100644 index 138b6972e..000000000 --- a/components/flags/to.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagTO from 'flag-icons/flags/1x1/to.svg' - -export const FlagTO = () => ( - -) -export default FlagTO diff --git a/components/flags/tr.js b/components/flags/tr.js deleted file mode 100644 index 7ea1214ad..000000000 --- a/components/flags/tr.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagTR from 'flag-icons/flags/1x1/tr.svg' - -export const FlagTR = () => ( - -) -export default FlagTR diff --git a/components/flags/tt.js b/components/flags/tt.js deleted file mode 100644 index 708aa6b0e..000000000 --- a/components/flags/tt.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagTT from 'flag-icons/flags/1x1/tt.svg' - -export const FlagTT = () => ( - -) -export default FlagTT diff --git a/components/flags/tv.js b/components/flags/tv.js deleted file mode 100644 index bf0e25a53..000000000 --- a/components/flags/tv.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagTV from 'flag-icons/flags/1x1/tv.svg' - -export const FlagTV = () => ( - -) -export default FlagTV diff --git a/components/flags/tw.js b/components/flags/tw.js deleted file mode 100644 index 832bf0244..000000000 --- a/components/flags/tw.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagTW from 'flag-icons/flags/1x1/tw.svg' - -export const FlagTW = () => ( - -) -export default FlagTW diff --git a/components/flags/tz.js b/components/flags/tz.js deleted file mode 100644 index 2f6fd8050..000000000 --- a/components/flags/tz.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagTZ from 'flag-icons/flags/1x1/tz.svg' - -export const FlagTZ = () => ( - -) -export default FlagTZ diff --git a/components/flags/ua.js b/components/flags/ua.js deleted file mode 100644 index fc4596480..000000000 --- a/components/flags/ua.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagUA from 'flag-icons/flags/1x1/ua.svg' - -export const FlagUA = () => ( - -) -export default FlagUA diff --git a/components/flags/ug.js b/components/flags/ug.js deleted file mode 100644 index 7e3989dbf..000000000 --- a/components/flags/ug.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagUG from 'flag-icons/flags/1x1/ug.svg' - -export const FlagUG = () => ( - -) -export default FlagUG diff --git a/components/flags/um.js b/components/flags/um.js deleted file mode 100644 index 9194e525b..000000000 --- a/components/flags/um.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagUM from 'flag-icons/flags/1x1/um.svg' - -export const FlagUM = () => ( - -) -export default FlagUM diff --git a/components/flags/un.js b/components/flags/un.js deleted file mode 100644 index d12fd9b6c..000000000 --- a/components/flags/un.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagUN from 'flag-icons/flags/1x1/un.svg' - -export const FlagUN = () => ( - -) -export default FlagUN diff --git a/components/flags/us.js b/components/flags/us.js deleted file mode 100644 index b23e919a3..000000000 --- a/components/flags/us.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagUS from 'flag-icons/flags/1x1/us.svg' - -export const FlagUS = () => ( - -) -export default FlagUS diff --git a/components/flags/uy.js b/components/flags/uy.js deleted file mode 100644 index f4f98184b..000000000 --- a/components/flags/uy.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagUY from 'flag-icons/flags/1x1/uy.svg' - -export const FlagUY = () => ( - -) -export default FlagUY diff --git a/components/flags/uz.js b/components/flags/uz.js deleted file mode 100644 index 3a8084a48..000000000 --- a/components/flags/uz.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagUZ from 'flag-icons/flags/1x1/uz.svg' - -export const FlagUZ = () => ( - -) -export default FlagUZ diff --git a/components/flags/va.js b/components/flags/va.js deleted file mode 100644 index 92c9710bb..000000000 --- a/components/flags/va.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagVA from 'flag-icons/flags/1x1/va.svg' - -export const FlagVA = () => ( - -) -export default FlagVA diff --git a/components/flags/vc.js b/components/flags/vc.js deleted file mode 100644 index 55e8fad6b..000000000 --- a/components/flags/vc.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagVC from 'flag-icons/flags/1x1/vc.svg' - -export const FlagVC = () => ( - -) -export default FlagVC diff --git a/components/flags/ve.js b/components/flags/ve.js deleted file mode 100644 index 60f6f7dac..000000000 --- a/components/flags/ve.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagVE from 'flag-icons/flags/1x1/ve.svg' - -export const FlagVE = () => ( - -) -export default FlagVE diff --git a/components/flags/vg.js b/components/flags/vg.js deleted file mode 100644 index 5a2bd09f0..000000000 --- a/components/flags/vg.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagVG from 'flag-icons/flags/1x1/vg.svg' - -export const FlagVG = () => ( - -) -export default FlagVG diff --git a/components/flags/vi.js b/components/flags/vi.js deleted file mode 100644 index 51b5a90fb..000000000 --- a/components/flags/vi.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagVI from 'flag-icons/flags/1x1/vi.svg' - -export const FlagVI = () => ( - -) -export default FlagVI diff --git a/components/flags/vn.js b/components/flags/vn.js deleted file mode 100644 index 1d0e30733..000000000 --- a/components/flags/vn.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagVN from 'flag-icons/flags/1x1/vn.svg' - -export const FlagVN = () => ( - -) -export default FlagVN diff --git a/components/flags/vu.js b/components/flags/vu.js deleted file mode 100644 index ef60d9c66..000000000 --- a/components/flags/vu.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagVU from 'flag-icons/flags/1x1/vu.svg' - -export const FlagVU = () => ( - -) -export default FlagVU diff --git a/components/flags/wf.js b/components/flags/wf.js deleted file mode 100644 index 8228d6830..000000000 --- a/components/flags/wf.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagWF from 'flag-icons/flags/1x1/wf.svg' - -export const FlagWF = () => ( - -) -export default FlagWF diff --git a/components/flags/ws.js b/components/flags/ws.js deleted file mode 100644 index 2a0e36d2f..000000000 --- a/components/flags/ws.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagWS from 'flag-icons/flags/1x1/ws.svg' - -export const FlagWS = () => ( - -) -export default FlagWS diff --git a/components/flags/ye.js b/components/flags/ye.js deleted file mode 100644 index 05af29bd9..000000000 --- a/components/flags/ye.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagYE from 'flag-icons/flags/1x1/ye.svg' - -export const FlagYE = () => ( - -) -export default FlagYE diff --git a/components/flags/yt.js b/components/flags/yt.js deleted file mode 100644 index 413d3c28b..000000000 --- a/components/flags/yt.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagYT from 'flag-icons/flags/1x1/yt.svg' - -export const FlagYT = () => ( - -) -export default FlagYT diff --git a/components/flags/za.js b/components/flags/za.js deleted file mode 100644 index aa10b3c79..000000000 --- a/components/flags/za.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagZA from 'flag-icons/flags/1x1/za.svg' - -export const FlagZA = () => ( - -) -export default FlagZA diff --git a/components/flags/zm.js b/components/flags/zm.js deleted file mode 100644 index ad6f8d881..000000000 --- a/components/flags/zm.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagZM from 'flag-icons/flags/1x1/zm.svg' - -export const FlagZM = () => ( - -) -export default FlagZM diff --git a/components/flags/zw.js b/components/flags/zw.js deleted file mode 100644 index 678b6ba26..000000000 --- a/components/flags/zw.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' - -import SvgFlagZW from 'flag-icons/flags/1x1/zw.svg' - -export const FlagZW = () => ( - -) -export default FlagZW diff --git a/components/landing/ChartLoader.js b/components/landing/ChartLoader.js index 0bec2e882..2934095c2 100644 --- a/components/landing/ChartLoader.js +++ b/components/landing/ChartLoader.js @@ -1,14 +1,13 @@ -import React from 'react' +import { colors } from 'ooni-components' import ContentLoader from 'react-content-loader' -import { theme } from 'ooni-components' export const ChartLoader = () => ( @@ -23,6 +22,5 @@ export const ChartLoader = () => ( - ) diff --git a/components/landing/HighlightBox.js b/components/landing/HighlightBox.js index 4d5bd7bd3..6e8ff0bdf 100644 --- a/components/landing/HighlightBox.js +++ b/components/landing/HighlightBox.js @@ -1,82 +1,34 @@ -import React from 'react' -import { useIntl } from 'react-intl' -import PropTypes from 'prop-types' -import { Flex, Box, Text, Heading, Link, theme } from 'ooni-components' -import styled from 'styled-components' import Markdown from 'markdown-to-jsx' +import PropTypes from 'prop-types' +import { useIntl } from 'react-intl' import { getLocalisedRegionName } from 'utils/i18nCountries' import Flag from '../Flag' -const StyledFlex = styled(Flex)` - min-height: 350px; - a:hover { - color: ${props => props.theme.colors.white}; - } -` - -const FlexGrowBox = styled(Box)` - flex-grow: ${props => props.grow || 1}; -` - -const HighlightBox = ({ - countryCode, - title, - text, - dates, - footer -}) => { +const HighlightBox = ({ countryCode, title, text, dates, footer }) => { const intl = useIntl() return ( - - +
+
{countryCode && ( - +
- +

{getLocalisedRegionName(countryCode, intl.locale)} - - +

+
)} {dates} - {/* {startDate && formatLongDate(startDate, intl.locale)} - {endDate ? formatLongDate(endDate, intl.locale) : 'ongoing'} */} - {title} - - - {text} - - - +

{title}

+

+ {text} +

+
{footer} - +
) } - - HighlightBox.propTypes = { countryCode: PropTypes.string, @@ -84,7 +36,7 @@ HighlightBox.propTypes = { title: PropTypes.string, text: PropTypes.string.isRequired, footer: PropTypes.element, - dates: PropTypes.element + dates: PropTypes.element, } export default HighlightBox diff --git a/components/landing/HighlightsSection.js b/components/landing/HighlightsSection.js index 7ebeba0e8..8c8691d34 100644 --- a/components/landing/HighlightsSection.js +++ b/components/landing/HighlightsSection.js @@ -1,90 +1,67 @@ -import NLink from 'next/link' -import { Box, Button, Flex, Text } from 'ooni-components' +import Link from 'next/link' import PropTypes from 'prop-types' -import React from 'react' import { useIntl } from 'react-intl' -import { styled } from 'styled-components' import HighlightBox from './HighlightBox' -const StyledGrid = styled(Box)` -display: grid; -grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); -gap: 24px; -` - -const HighlightSection = ({ - title, - highlights, - description -}) => { +const HighlightSection = ({ title, highlights, description }) => { const intl = useIntl() return (
- - - - {title} - - - +
{title}
{/* Optional Description */} - {description && - {description} - } - + {description &&
{description}
} +
{/* HighlightBoxes */} - { - highlights.map((item, index) => ( - - {item.explore && - - - - - - } - {item.report && - - - - - - } - - } - /> - )) - } - + {highlights.map((item, index) => ( + + {item.explore && ( + + + + )} + {item.report && ( + + + + )} +
+ } + /> + ))} +
) } HighlightSection.propTypes = { - title: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.element - ]), - description: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.element - ]), - highlights: PropTypes.arrayOf(PropTypes.shape({ - countryCode: PropTypes.string.isRequired, - title: PropTypes.string, - text: PropTypes.string, - report: PropTypes.string, - explore: PropTypes.string - })) + title: PropTypes.oneOfType([PropTypes.string, PropTypes.element]), + description: PropTypes.oneOfType([PropTypes.string, PropTypes.element]), + highlights: PropTypes.arrayOf( + PropTypes.shape({ + countryCode: PropTypes.string.isRequired, + title: PropTypes.string, + text: PropTypes.string, + report: PropTypes.string, + explore: PropTypes.string, + }), + ), } export default HighlightSection diff --git a/components/landing/Stats.js b/components/landing/Stats.js index 4265b3492..341765cfa 100644 --- a/components/landing/Stats.js +++ b/components/landing/Stats.js @@ -1,18 +1,17 @@ /* global process */ -import React from 'react' +import axios from 'axios' +import { colors } from 'ooni-components' +import { useIntl } from 'react-intl' +import dayjs from 'services/dayjs' +import useSWR from 'swr' import { - VictoryChart, - VictoryLine, - VictoryAxis, createContainer, LineSegment, - VictoryLegend + VictoryAxis, + VictoryChart, + VictoryLegend, + VictoryLine, } from 'victory' -import axios from 'axios' -import dayjs from 'services/dayjs' -import { Flex, Text, theme } from 'ooni-components' -import { useIntl } from 'react-intl' -import useSWR from 'swr' import Tooltip from '../country/Tooltip' import FormattedMarkdown from '../FormattedMarkdown' @@ -22,8 +21,7 @@ import { ChartLoader } from './ChartLoader' const getMaxima = (data) => { let maxima data.forEach((d) => { - if (typeof maxima === 'undefined' - || maxima < d.value) { + if (typeof maxima === 'undefined' || maxima < d.value) { maxima = d.value } }) @@ -32,40 +30,44 @@ const getMaxima = (data) => { const BASE_URL = `${process.env.NEXT_PUBLIC_OONI_API}` -const dataFetcher = query => ( - axios.get( BASE_URL + query).then(r => r.data) -) +const dataFetcher = (query) => axios.get(BASE_URL + query).then((r) => r.data) const swrOptions = { revalidateOnFocus: false, } const getLastMonthData = (collection) => { - const cc = collection.filter(d => { + const cc = collection.filter((d) => { const dt = new Date(Date.parse(d.date)) - dt.setUTCMonth(dt.getUTCMonth()+1) + dt.setUTCMonth(dt.getUTCMonth() + 1) const today = new Date() - return (dt.getUTCFullYear() === today.getUTCFullYear()) && (today.getUTCMonth() === dt.getUTCMonth()) + return ( + dt.getUTCFullYear() === today.getUTCFullYear() && + today.getUTCMonth() === dt.getUTCMonth() + ) }) return cc.length === 0 ? 0 : cc[0]?.value ?? 0 } const CoverageChart = () => { - - const { data, isValidating } = useSWR('/api/_/global_overview_by_month', dataFetcher, swrOptions) + const { data, isValidating } = useSWR( + '/api/_/global_overview_by_month', + dataFetcher, + swrOptions, + ) const intl = useIntl() if (data) { - const countryCoverage = data.countries_by_month.slice(0, -1), - networkCoverage = data.networks_by_month.slice(0, -1), - measurementsByMonth = data.measurements_by_month.slice(0, -1) + const countryCoverage = data.countries_by_month.slice(0, -1) + const networkCoverage = data.networks_by_month.slice(0, -1) + const measurementsByMonth = data.measurements_by_month.slice(0, -1) // API responses are ordered by date, with most recent month at the end const lastMonth = { countryCount: getLastMonthData(data.countries_by_month), networkCount: getLastMonthData(data.networks_by_month), - measurementCount: getLastMonthData(data.measurements_by_month) + measurementCount: getLastMonthData(data.measurements_by_month), } // Determine the maximum value for each data set @@ -78,18 +80,17 @@ const CoverageChart = () => { return ( <> - - - - - +
+ +
{ } - voronoiDimension='x' + voronoiDimension="x" labels={(d) => { if (d.childName === 'countryCoverage') { return `${d.date}\n \nCountries: ${d.value}` - } else if (d.childName === 'networkCoverage') { + } + if (d.childName === 'networkCoverage') { return `Networks: ${d.value}` - } else if (d.childName === 'measurementsByMonth') { + } + if (d.childName === 'measurementsByMonth') { return `Measurements: ${d.value}` } }} @@ -115,60 +121,65 @@ const CoverageChart = () => { /> } domainPadding={{ - x: 0, y: 10 + x: 0, + y: 10, }} > dayjs(t).format('MMM\'YY')} + tickFormat={(t) => dayjs(t).format("MMM'YY")} /> Math.floor(t * countryCoverageMaxima)} /> d.value / countryCoverageMaxima} scale={{ x: 'time', y: 'linear' }} style={{ data: { - stroke: theme.colors.blue8 - } + stroke: colors.blue['800'], + }, }} /> { offsetX={400} style={{ axis: { - stroke : theme.colors.gray7, - strokeWidth: 2 - } + stroke: colors.gray['700'], + strokeWidth: 2, + }, }} tickValues={[0, 0.5, 1]} // Hide tick value 0 for the axis in the middle of the chart - tickFormat={(t) => t > 0 ? Math.floor(t * networkCoverageMaxima) : ''} + tickFormat={(t) => + t > 0 ? Math.floor(t * networkCoverageMaxima) : '' + } /> (d.value + 20) / networkCoverageMaxima} scale={{ x: 'time', y: 'linear' }} style={{ data: { - stroke: theme.colors.gray7 - } + stroke: colors.gray['700'], + }, }} /> `${Math.round(t * measurementMaxima/1000, 2)}k`} + tickFormat={(t) => + `${Math.round((t * measurementMaxima) / 1000, 2)}k` + } /> (d.value + 20) / measurementMaxima} scale={{ x: 'time', y: 'linear' }} style={{ data: { - stroke: theme.colors.yellow7 - } + stroke: colors.yellow['700'], + }, }} /> ) - } else { - return () } + return } export default CoverageChart diff --git a/components/login/LoginForm.js b/components/login/LoginForm.js index d3fb31c9d..35b809993 100644 --- a/components/login/LoginForm.js +++ b/components/login/LoginForm.js @@ -1,60 +1,44 @@ -import { Box, Button, Flex, Input } from 'ooni-components' -import React, { useCallback, useEffect, useState } from 'react' +import { Input } from 'ooni-components' +import { useCallback, useEffect, useState } from 'react' import { Controller, useForm } from 'react-hook-form' -import styled from 'styled-components' import SpinLoader from 'components/vendor/SpinLoader' -import { useRouter } from 'next/router' import { FormattedMessage } from 'react-intl' import { registerUser } from '/lib/api' -const StyledError = styled.small` - color: ${props => props.theme.colors.red5}; -` - -const StyledInputContainer = styled(Box).attrs({ - width: '100%', - mt: 3, -})` - position: relative; - & ${StyledError} { - position: absolute; - top: -10px; - right: 0px; - } -` - export const LoginForm = ({ onLogin, redirectTo }) => { - const router = useRouter() const [submitting, setSubmitting] = useState(false) const [loginError, setError] = useState(null) const { handleSubmit, control, formState, reset } = useForm({ mode: 'onTouched', - defaultValues: { email_address: '' } + defaultValues: { email_address: '' }, }) const { errors, isValid, isDirty } = formState - const onSubmit = useCallback((data) => { - const { email_address } = data - const registerApi = async (email_address) => { - try { - await registerUser(email_address, redirectTo) - if (typeof onLogin === 'function') { - onLogin() + const onSubmit = useCallback( + (data) => { + const { email_address } = data + const registerApi = async (email_address) => { + try { + await registerUser(email_address, redirectTo) + if (typeof onLogin === 'function') { + onLogin() + } + } catch (e) { + setError(e.message) + // Reset form to mark `isDirty` as false + reset({}, { keepValues: true }) + } finally { + setSubmitting(false) } - } catch (e) { - setError(e.message) - // Reset form to mark `isDirty` as false - reset({}, { keepValues: true }) - } finally { - setSubmitting(false) } - } - setSubmitting(true) - registerApi(email_address) - }, [onLogin, reset, redirectTo]) + setSubmitting(true) + registerApi(email_address) + }, + [onLogin, reset, redirectTo], + ) useEffect(() => { // Remove previous errors when form becomes dirty again @@ -65,40 +49,45 @@ export const LoginForm = ({ onLogin, redirectTo }) => { return (
- - - ( - +
+ ( + )} rules={{ pattern: { - value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i, + value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i, }, required: true, }} - name='email_address' + name="email_address" control={control} /> - {errors?.email_address?.message} - - {loginError && - - {loginError} - - } - - {!submitting ? - : - - } - - +
+ {loginError && ( +
+ {loginError} +
+ )} +
+ {!submitting ? ( + + ) : ( + + )} +
+ ) } diff --git a/components/measurement/AccessPointStatus.js b/components/measurement/AccessPointStatus.js index 969e10171..33ecde173 100644 --- a/components/measurement/AccessPointStatus.js +++ b/components/measurement/AccessPointStatus.js @@ -1,48 +1,39 @@ -import React from 'react' import PropTypes from 'prop-types' -import { Box, Text } from 'ooni-components' import { FormattedMessage } from 'react-intl' -import styled from 'styled-components' -const StatusText = styled(Text)` - color: ${props => props.$ok === false ? props.theme.colors.yellow9 : 'unset'} -` - -const AccessPointStatus = ({ icon, label, ok, content, color, ...props}) => { +const AccessPointStatus = ({ icon, label, ok, content, color, ...props }) => { if (content === undefined) { if (ok === true) { - content = - } else if (ok === false){ - content = + content = + } else if (ok === false) { + content = } else { - content = + content = ( + + ) } } return ( - +
{icon} - {label} - {label}
+ {/* to fix below */} +
{content} - - +
+ ) } AccessPointStatus.propTypes = { icon: PropTypes.element, - label: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.element - ]).isRequired, + label: PropTypes.oneOfType([PropTypes.string, PropTypes.element]).isRequired, ok: PropTypes.oneOf([true, false, undefined]), - content: PropTypes.any + content: PropTypes.any, } export default AccessPointStatus diff --git a/components/measurement/CommonDetails.js b/components/measurement/CommonDetails.js index 5d9c0f8a1..b837e7349 100644 --- a/components/measurement/CommonDetails.js +++ b/components/measurement/CommonDetails.js @@ -1,55 +1,40 @@ -/* global process */ -import React, { useState } from 'react' -import PropTypes from 'prop-types' -import styled from 'styled-components' -import { - Heading, - Button, - Flex, - Box, - Link, - theme -} from 'ooni-components' import dynamic from 'next/dynamic' -import { FormattedMessage, useIntl } from 'react-intl' import { useRouter } from 'next/router' +import { colors } from 'ooni-components' +import PropTypes from 'prop-types' +import { useState } from 'react' +import { FormattedMessage, useIntl } from 'react-intl' -import { DetailsBoxTable, DetailsBox } from './DetailsBox' +import { DetailsBox, DetailsBoxTable } from './DetailsBox' -const LoadingRawData = (props) => { - return () +const LoadingRawData = () => { + return ( +
+ +
+ ) } -const ReactJson = dynamic( - () => import('react-json-view'), - { ssr: false, loading: LoadingRawData } - -) - -const StyledReactJsonContainer = styled.div` - .string-value { - text-overflow: ellipsis; - max-width: 800px; - overflow: hidden; - display: inline-block; - } -` +const ReactJson = dynamic(() => import('react-json-view'), { + ssr: false, + loading: LoadingRawData, +}) const JsonViewer = ({ src, collapsed }) => ( - +
- +
) JsonViewer.propTypes = { - src: PropTypes.object.isRequired + src: PropTypes.object.isRequired, } const CommonDetails = ({ measurement, reportId, measurementUid, - userFeedbackItems =[] + userFeedbackItems = [], }) => { const { software_name, @@ -66,12 +51,14 @@ const CommonDetails = ({ const [collapsed, setCollapsed] = useState(1) const intl = useIntl() - const unavailable = intl.formatMessage({ id: 'Measurement.CommonDetails.Value.Unavailable' }) + const unavailable = intl.formatMessage({ + id: 'Measurement.CommonDetails.Value.Unavailable', + }) let engine = unavailable let platform = unavailable - if (annotations && annotations.engine_name) { + if (annotations?.engine_name) { engine = annotations.engine_name if (annotations.engine_version) { @@ -79,7 +66,7 @@ const CommonDetails = ({ } } - if (annotations && annotations.platform) { + if (annotations?.platform) { platform = annotations.platform } @@ -89,40 +76,56 @@ const CommonDetails = ({ const downloadFilename = `ooni-measurement-${measurementUid}.json` const items = [ { - label: intl.formatMessage({ id: 'Measurement.CommonDetails.Label.MsmtID' }), - value: measurementUid ?? unavailable + label: intl.formatMessage({ + id: 'Measurement.CommonDetails.Label.MsmtID', + }), + value: measurementUid ?? unavailable, }, { - label: intl.formatMessage({ id: 'Measurement.CommonDetails.Label.ReportID' }), - value: reportId ?? unavailable + label: intl.formatMessage({ + id: 'Measurement.CommonDetails.Label.ReportID', + }), + value: reportId ?? unavailable, }, { - label: intl.formatMessage({ id: 'Measurement.CommonDetails.Label.Platform' }), - value: platform + label: intl.formatMessage({ + id: 'Measurement.CommonDetails.Label.Platform', + }), + value: platform, }, { - label: intl.formatMessage({ id: 'Measurement.CommonDetails.Label.Software' }), - value: software + label: intl.formatMessage({ + id: 'Measurement.CommonDetails.Label.Software', + }), + value: software, }, { - label: intl.formatMessage({ id: 'Measurement.CommonDetails.Label.Engine' }), - value: engine - } + label: intl.formatMessage({ + id: 'Measurement.CommonDetails.Label.Engine', + }), + value: engine, + }, ] const showResolverItems = resolver_asn || resolver_ip || resolver_network_name const resolverItems = [ { - label: intl.formatMessage({ id: 'Measurement.CommonDetails.Label.ResolverASN' }), - value: resolver_asn ?? unavailable + label: intl.formatMessage({ + id: 'Measurement.CommonDetails.Label.ResolverASN', + }), + value: resolver_asn ?? unavailable, }, { - label: intl.formatMessage({ id: 'Measurement.CommonDetails.Label.ResolverIP' }), - value: resolver_ip ?? unavailable + label: intl.formatMessage({ + id: 'Measurement.CommonDetails.Label.ResolverIP', + }), + value: resolver_ip ?? unavailable, }, { - label: intl.formatMessage({ id: 'Measurement.CommonDetails.Label.ResolverNetworkName' }), - value: resolver_network_name ?? unavailable + label: intl.formatMessage({ + id: 'Measurement.CommonDetails.Label.ResolverNetworkName', + }), + value: resolver_network_name ?? unavailable, }, ] @@ -133,75 +136,86 @@ const CommonDetails = ({ return ( <> - {showResolverItems && - + {showResolverItems && ( +
{/* Resolver data */} } + title={ + + } items={resolverItems} /> - - } - +
+ )} +
{/* Metadata: platform, probe, MK version etc. */} - - + +
{/* User Feedback */} - {!!userFeedbackItems.length && - + {!!userFeedbackItems.length && ( +
} + title={ + + } items={userFeedbackItems} /> - - } +
+ )} {/* Raw Measurement */} - + +
*/} + - - + {intl.formatMessage({ + id: 'Measurement.CommonDetails.RawMeasurement.Expand', + })} + +
+ } content={ measurement && typeof measurement === 'object' ? ( - +
- +
) : ( - + ) } /> -
+ ) } @@ -209,7 +223,7 @@ const CommonDetails = ({ CommonDetails.propTypes = { measurement: PropTypes.object, reportId: PropTypes.string, - measurementUid: PropTypes.string + measurementUid: PropTypes.string, } export default CommonDetails diff --git a/components/measurement/CommonSummary.js b/components/measurement/CommonSummary.js index 723d3a7c5..c6c8e4e89 100644 --- a/components/measurement/CommonSummary.js +++ b/components/measurement/CommonSummary.js @@ -1,30 +1,9 @@ -import NLink from 'next/link' -import { - Box, - Container, - Flex, - Text -} from 'ooni-components' +import Link from 'next/link' import PropTypes from 'prop-types' -import React from 'react' import { MdOutlineFactCheck } from 'react-icons/md' import { useIntl } from 'react-intl' -import styled from 'styled-components' - import Flag from '../Flag' -const SummaryContainer = styled(Box)` - background-color: ${props => props.color}; - color: white; -` - -const StyledLink = styled(NLink)` - color: white; - &:hover { - color: white; - } -` - const CommonSummary = ({ color, measurement_start_time, @@ -33,58 +12,64 @@ const CommonSummary = ({ networkName, country, hero, - onVerifyClick + onVerifyClick, }) => { const intl = useIntl() const startTime = measurement_start_time const network = probe_asn const countryCode = probe_cc - const formattedDate = new Intl.DateTimeFormat(intl.locale, { dateStyle: 'long', timeStyle: 'long', timeZone: 'UTC' }).format(new Date(startTime)) - + const formattedDate = new Intl.DateTimeFormat(intl.locale, { + dateStyle: 'long', + timeStyle: 'long', + timeZone: 'UTC', + }).format(new Date(startTime)) + return ( - <> - - - - - {formattedDate} - - - - {/* - - {'Share'.toUpperCase()} - */} - - - {intl.formatMessage({id: 'Measurement.CommonSummary.Verify'}).toUpperCase()} - - - - - {hero} - - - - - - - - - {country} - - - - - - {network} {networkName} - - - - - - - +
+
+
+
{formattedDate}
+
+
+
+ +
+
+ {intl + .formatMessage({ id: 'Measurement.CommonSummary.Verify' }) + .toUpperCase()} +
+
+
+
+ {hero} +
+
+ +
+
+ +
+ {country} +
+ + + {network} {networkName} + +
+
+
+
) } @@ -94,7 +79,7 @@ CommonSummary.propTypes = { probe_cc: PropTypes.string.isRequired, networkName: PropTypes.string, country: PropTypes.string.isRequired, - color: PropTypes.string.isRequired + color: PropTypes.string.isRequired, } export default CommonSummary diff --git a/components/measurement/DetailsBox.js b/components/measurement/DetailsBox.js index 4806071ad..14c1f05a8 100644 --- a/components/measurement/DetailsBox.js +++ b/components/measurement/DetailsBox.js @@ -1,63 +1,38 @@ -import React, { useState, useCallback } from 'react' import PropTypes from 'prop-types' -import styled from 'styled-components' -import { Flex, Box, Text, Heading } from 'ooni-components' -import { FormattedMessage } from 'react-intl' -import { CollapseTrigger } from '../CollapseTrigger' +import { useCallback, useState } from 'react' +import { MdExpandLess } from 'react-icons/md' -const DetailBoxLabel = styled(Text)` - font-weight: 600; -` - -const DetailBoxValue = styled(Text)` - overflow-wrap: break-word; -` - -export const DetailsBoxTable = ({ - title, - items, - bg -}) => ( - - - - {item.label} - - - {item.value} - - - )} +export const DetailsBoxTable = ({ title, items, bg }) => ( + ( +
+
{item.label}
+
{item.value}
+
+ ))} /> ) DetailsBoxTable.propTypes = { - title: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.element - ]), - items: PropTypes.arrayOf(PropTypes.shape({ - label: PropTypes.string.isRequired, - value: PropTypes.string - })), - bg: PropTypes.string + title: PropTypes.oneOfType([PropTypes.string, PropTypes.element]), + items: PropTypes.arrayOf( + PropTypes.shape({ + label: PropTypes.string.isRequired, + value: PropTypes.string, + }), + ), + bg: PropTypes.string, } -const StyledDetailsBox = styled(Box)` - border: 2px solid ${props => props.theme.colors.gray2}; -` - -const StyledDetailsBoxHeader = styled(Flex)` - cursor: pointer; - justify-content: space-between; -` - -const StyledDetailsBoxContent = styled(Box)` - overflow-x: auto; -` - -export const DetailsBox = ({ title, content, collapsed = false, children, ...rest }) => { +export const DetailsBox = ({ + title, + content, + collapsed = false, + children, + ...rest +}) => { const [isOpen, setIsOpen] = useState(!collapsed) const onToggle = useCallback(() => { @@ -65,31 +40,30 @@ export const DetailsBox = ({ title, content, collapsed = false, children, ...res }, [isOpen, setIsOpen]) return ( - - {title && - - - {title} - - - - - - } - {isOpen && - +
+ {title && ( +
+ {title} + +
+ )} + {isOpen && ( +
{content || children} - - } - +
+ )} +
) } DetailsBox.propTypes = { - title: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.element - ]), + title: PropTypes.oneOfType([PropTypes.string, PropTypes.element]), content: PropTypes.node, - collapsed: PropTypes.bool + collapsed: PropTypes.bool, } diff --git a/components/measurement/DetailsHeader.js b/components/measurement/DetailsHeader.js index e3432f570..858059f0c 100644 --- a/components/measurement/DetailsHeader.js +++ b/components/measurement/DetailsHeader.js @@ -1,48 +1,41 @@ -import React from 'react' -import PropTypes from 'prop-types' import prettyMs from 'pretty-ms' -import { Flex, Box, Link, Text } from 'ooni-components' -import { FormattedMessage } from 'react-intl' +import PropTypes from 'prop-types' import { MdOpenInNew } from 'react-icons/md' +import { FormattedMessage } from 'react-intl' import { getTestMetadata } from '../utils' import TestGroupBadge from '../Badge' import SocialButtons from '../SocialButtons' -const DetailsHeader = ({testName, runtime, notice, url}) => { +const DetailsHeader = ({ testName, runtime, notice, url }) => { const metadata = getTestMetadata(testName) return ( <> - - - - - - - - - {metadata.name} -   - - - - - - - {notice} - - - {runtime && - - : {prettyMs(runtime * 1000)} - - } - - - - - +
+ +
{notice}
+ {runtime && ( +
+ :{' '} + {prettyMs(runtime * 1000)} +
+ )} +
+
+ +
) } @@ -50,7 +43,7 @@ const DetailsHeader = ({testName, runtime, notice, url}) => { DetailsHeader.propTypes = { testName: PropTypes.string.isRequired, runtime: PropTypes.number.isRequired, - notice: PropTypes.any + notice: PropTypes.any, } export default DetailsHeader diff --git a/components/measurement/FeedbackBox.js b/components/measurement/FeedbackBox.js index d13832b93..dd519e648 100644 --- a/components/measurement/FeedbackBox.js +++ b/components/measurement/FeedbackBox.js @@ -1,71 +1,69 @@ -import React, { useEffect, useMemo, useRef, useState } from 'react' -import { Controller, useForm } from 'react-hook-form' -import { FormattedMessage, useIntl } from 'react-intl' -import { Box, Button, Flex, Text, theme, RadioButton, RadioGroup } from 'ooni-components' -import { GrClose } from 'react-icons/gr' import useStateMachine from '@cassiozen/usestatemachine' +import LoginForm from 'components/login/LoginForm' import SpinLoader from 'components/vendor/SpinLoader' import { submitFeedback } from 'lib/api' -import LoginForm from 'components/login/LoginForm' -import styled from 'styled-components' - -const StyledCloseIcon = styled(GrClose)` -position: absolute; -top: 16px; -right: 16px; -font-size: 16px; -cursor: pointer; -` - -const StyledError = styled.small` - color: ${props => props.theme.colors.red5}; -` +import { RadioButton, RadioGroup, colors } from 'ooni-components' +import { useEffect, useMemo, useRef, useState } from 'react' +import { Controller, useForm } from 'react-hook-form' +import { GrClose } from 'react-icons/gr' +import { FormattedMessage, useIntl } from 'react-intl' const blockedValues = [ { top: 'ok' }, - { top: 'down', - sub: [ - { top: 'down.unreachable' }, - { top: 'down.misconfigured' }, - ] + { + top: 'down', + sub: [{ top: 'down.unreachable' }, { top: 'down.misconfigured' }], }, - { top: 'blocked', + { + top: 'blocked', sub: [ - { + { top: 'blocked.blockpage', sub: [ 'blocked.blockpage.http', 'blocked.blockpage.dns', 'blocked.blockpage.server_side', - 'blocked.blockpage.server_side.captcha' - ] + 'blocked.blockpage.server_side.captcha', + ], }, - { top: 'blocked.dns', - sub: ['blocked.dns.inconsistent', 'blocked.dns.nxdomain'] + { + top: 'blocked.dns', + sub: ['blocked.dns.inconsistent', 'blocked.dns.nxdomain'], }, { top: 'blocked.tcp' }, - { top: 'blocked.tls' } - ] + { top: 'blocked.tls' }, + ], }, ] const radioLevels = ['firstLevelRadio', 'secondLevelRadio', 'thirdLevelRadio'] -const FeedbackBox = ({user, measurement_uid, setShowModal, previousFeedback, mutateUserFeedback}) => { +const FeedbackBox = ({ + user, + measurement_uid, + setShowModal, + previousFeedback, + mutateUserFeedback, +}) => { const intl = useIntl() const [error, setError] = useState(null) const redirectTo = typeof window !== 'undefined' && window.location.href - + const defaultValues = useMemo(() => { if (!previousFeedback) return {} const feedbackLevels = previousFeedback.split('.') return feedbackLevels.reduce((acc, current, i) => { - return {...acc, ...{[radioLevels[i]]: feedbackLevels.slice(0, i+1).join('.')}} + return { + ...acc, + ...{ [radioLevels[i]]: feedbackLevels.slice(0, i + 1).join('.') }, + } }, {}) }, [previousFeedback]) - - const { handleSubmit, control, watch, setValue, getValues } = useForm({defaultValues}) + + const { handleSubmit, control, watch, setValue, getValues } = useForm({ + defaultValues, + }) const [state, send] = useStateMachine({ initial: 'initial', @@ -77,19 +75,19 @@ const FeedbackBox = ({user, measurement_uid, setShowModal, previousFeedback, mut }, on: { LOGIN: 'login', - FEEDBACK: 'feedback' + FEEDBACK: 'feedback', }, }, login: { on: { LOGIN_SUCCESS: 'loginSuccess', - } + }, }, loginSuccess: {}, feedback: { on: { CANCEL: 'initial', - SUBMIT: 'submit' + SUBMIT: 'submit', }, effect() { return () => setError(null) @@ -97,10 +95,11 @@ const FeedbackBox = ({user, measurement_uid, setShowModal, previousFeedback, mut }, submit: { effect({ send, setContext, event, context }) { - const { firstLevelRadio, secondLevelRadio, thirdLevelRadio } = getValues() + const { firstLevelRadio, secondLevelRadio, thirdLevelRadio } = + getValues() const feedbackParams = { measurement_uid, - status: thirdLevelRadio || secondLevelRadio || firstLevelRadio + status: thirdLevelRadio || secondLevelRadio || firstLevelRadio, } submitFeedback(feedbackParams) .then(() => { @@ -109,27 +108,28 @@ const FeedbackBox = ({user, measurement_uid, setShowModal, previousFeedback, mut }) .catch((response) => { setError(response?.message || 'unknown') - }).finally(() => send('FEEDBACK')) + }) + .finally(() => send('FEEDBACK')) }, on: { SUCCESS: 'success', - FEEDBACK: 'feedback' - } + FEEDBACK: 'feedback', + }, }, success: {}, }, }) - const initialLoadFirst = useRef(false) const initialLoadSecond = useRef(false) const firstLevelRadio = watch(radioLevels[0]) const secondLevelRadio = watch(radioLevels[1]) - + useEffect(() => { if (initialLoadFirst.current) return setValue(radioLevels[1], null) initialLoadFirst.current = true }, [firstLevelRadio]) + useEffect(() => { if (initialLoadSecond.current) return setValue(radioLevels[2], null) initialLoadSecond.current = true @@ -138,153 +138,189 @@ const FeedbackBox = ({user, measurement_uid, setShowModal, previousFeedback, mut const submitEnabled = useMemo(() => !!firstLevelRadio, [firstLevelRadio]) return ( - <> - - setShowModal(false)} /> - <> - {state.value === 'initial' && - <> - - - - - - - - - - - } - {state.value === 'login' && - <> - - - - - {send('LOGIN_SUCCESS')}} redirectTo={redirectTo} /> - - } - {state.value === 'loginSuccess' && - <> - - - - - - - - - } - {state.value === 'feedback' && -
e.preventDefault()}> - - - - ( - - {blockedValues.map(({top, sub}) => ( - <> - - {sub && firstLevelRadio === top && ( - ( - - {sub.map(({top, sub}) => ( - <> - - {sub && secondLevelRadio === top && ( - ( - - {sub.map(subVal => ( - ) - )} - - )} - />)} - - ))} - - )} - />)} - - ))} - - )} +
+ setShowModal(false)} + /> + <> + {state.value === 'initial' && ( + <> +
+ +
+
+ - - - - {error && - - - Error: {error} - - } - - - - } - {state.value === 'submit' && - - } - {state.value === 'success' && - <> - - - - - - - } - - - +
+
+ +
+ + )} + {state.value === 'login' && ( + <> +
+ +
+
+ +
+ { + send('LOGIN_SUCCESS') + }} + redirectTo={redirectTo} + /> + + )} + {state.value === 'loginSuccess' && ( + <> +
+ +
+
+ +
+ + + )} + {state.value === 'feedback' && ( +
e.preventDefault()}> +
+ +
+ ( + + {blockedValues.map(({ top, sub }) => ( + <> + + {sub && firstLevelRadio === top && ( + ( + + {sub.map(({ top, sub }) => ( + <> + + {sub && secondLevelRadio === top && ( + ( + + {sub.map((subVal) => ( + + ))} + + )} + /> + )} + + ))} + + )} + /> + )} + + ))} + + )} + /> +
+ +
+ {error && ( + + +
+ Error: {error} +
+
+ )} + + + + )} + {state.value === 'submit' && ( + + )} + {state.value === 'success' && ( + <> +
+ +
+
+ +
+
+ +
+ + )} + +
) } -export default FeedbackBox \ No newline at end of file +export default FeedbackBox diff --git a/components/measurement/HeadMetadata.js b/components/measurement/HeadMetadata.js index 7dedfcf23..7dea84a14 100644 --- a/components/measurement/HeadMetadata.js +++ b/components/measurement/HeadMetadata.js @@ -1,58 +1,48 @@ -import React from 'react' import Head from 'next/head' import PropTypes from 'prop-types' import { useIntl } from 'react-intl' -import dayjs from 'services/dayjs' import { getTestMetadata } from '../utils' -const HeadMetadata = ({ - testName, - // network, - country, - date, - content -}) => { +const HeadMetadata = ({ testName, country, date, content }) => { const intl = useIntl() let description = '' - const formattedDate = new Intl.DateTimeFormat(intl.locale, { dateStyle: 'long', timeStyle: 'long', timeZone: 'UTC' }).format(new Date(date)) + const formattedDate = new Intl.DateTimeFormat(intl.locale, { + dateStyle: 'long', + timeStyle: 'long', + timeZone: 'UTC', + }).format(new Date(date)) if (content.formatted) { description = content.message } else { const metadata = getTestMetadata(testName) - description = intl.formatMessage( - content.message, - { - testName: metadata.name, - country: country, - date: formattedDate - } - ) + description = intl.formatMessage(content.message, { + testName: metadata.name, + country: country, + date: formattedDate, + }) } - const metaDescription = intl.formatMessage({ - id: 'Measurement.MetaDescription'}, + const metaDescription = intl.formatMessage( + { + id: 'Measurement.MetaDescription', + }, { description, - formattedDate - } + formattedDate, + }, ) return ( - - {description} - + {description} - + ) } @@ -62,12 +52,12 @@ HeadMetadata.propTypes = { message: PropTypes.oneOfType([ PropTypes.string, PropTypes.shape({ - id: PropTypes.string, - defaultMessage: PropTypes.string - }) + id: PropTypes.string, + defaultMessage: PropTypes.string, + }), ]).isRequired, - formatted: PropTypes.bool.isRequired - }) + formatted: PropTypes.bool.isRequired, + }), } export default HeadMetadata diff --git a/components/measurement/Hero.js b/components/measurement/Hero.js index 7ad27612a..83e5827c2 100644 --- a/components/measurement/Hero.js +++ b/components/measurement/Hero.js @@ -1,42 +1,37 @@ -import React from 'react' +import { Cross, Tick } from 'ooni-components/icons' import PropTypes from 'prop-types' -import { Container, Flex, Box, Text } from 'ooni-components' -import { Tick, Cross } from 'ooni-components/icons' -import { MdWarning, MdPriorityHigh} from 'react-icons/md' import { FaQuestion } from 'react-icons/fa' -import styled from 'styled-components' +import { MdPriorityHigh, MdWarning } from 'react-icons/md' import { FormattedMessage } from 'react-intl' -const HeroContainer = styled(Box)` - color: white; -` - const Hero = ({ status, icon, label, info }) => { let computedLabel = '' if (status) { switch (status) { - case 'anomaly': - computedLabel = - icon = - break - case 'reachable': - computedLabel = - icon = - break - case 'error': - computedLabel = - icon = - break - case 'confirmed': - computedLabel = - icon = - break - case 'down': - computedLabel = - icon = - break - default: - icon = icon ||
+ case 'anomaly': + computedLabel = + icon = + break + case 'reachable': + computedLabel = + icon = + break + case 'error': + computedLabel = + icon = + break + case 'confirmed': + computedLabel = ( + + ) + icon = + break + case 'down': + computedLabel = + icon = + break + default: + icon = icon ||
} } @@ -45,20 +40,16 @@ const Hero = ({ status, icon, label, info }) => { } return ( - - - - - {icon} {label} - - - {info && - - {info} - - } - - +
+
+
+
+ {icon}
{label}
+
+
+ {info &&
{info}
} +
+
) } @@ -66,6 +57,6 @@ Hero.propTypes = { status: PropTypes.string, icon: PropTypes.node, label: PropTypes.string, - info: PropTypes.node + info: PropTypes.node, } export default Hero diff --git a/components/measurement/InfoBoxItem.js b/components/measurement/InfoBoxItem.js index ef1206438..68f399b85 100644 --- a/components/measurement/InfoBoxItem.js +++ b/components/measurement/InfoBoxItem.js @@ -1,31 +1,23 @@ -import React from 'react' import PropTypes from 'prop-types' -import { Box, Text } from 'ooni-components' import { FormattedMessage } from 'react-intl' -export const InfoBoxItem = ({ - label, - content, - unit -}) => ( - - - {content} {unit && {unit}} - - - {label} - - +export const InfoBoxItem = ({ label, content, unit }) => ( +
+
+ {content} {unit && {unit}} +
+
{label}
+
) InfoBoxItem.propTypes = { label: PropTypes.oneOfType([ PropTypes.string, - PropTypes.instanceOf(FormattedMessage) + PropTypes.instanceOf(FormattedMessage), ]), content: PropTypes.any, unit: PropTypes.oneOfType([ PropTypes.string, - PropTypes.instanceOf(FormattedMessage) - ]) + PropTypes.instanceOf(FormattedMessage), + ]), } diff --git a/components/measurement/MeasurementContainer.js b/components/measurement/MeasurementContainer.js index 8546c3bc8..693b7844c 100644 --- a/components/measurement/MeasurementContainer.js +++ b/components/measurement/MeasurementContainer.js @@ -1,18 +1,17 @@ -import React from 'react' import PropTypes from 'prop-types' -import WebConnectivityDetails from './nettests/WebConnectivity' -import TelegramDetails from './nettests/Telegram' -import WhatsAppDetails from './nettests/WhatsApp' import DashDetails from './nettests/Dash' -import NdtDetails from './nettests/Ndt' -import SignalDetails from './nettests/Signal' import FacebookMessengerDetails from './nettests/FacebookMessenger' import HttpHeaderFieldManipulationDetails from './nettests/HTTPHeaderFieldManipulation' import HttpInvalidRequestLine from './nettests/HTTPInvalidRequestLine' +import NdtDetails from './nettests/Ndt' +import SignalDetails from './nettests/Signal' +import TelegramDetails from './nettests/Telegram' +import WebConnectivityDetails from './nettests/WebConnectivity' +import WhatsAppDetails from './nettests/WhatsApp' -import VanillaTorDetails from './nettests/VanillaTor' import PsiphonDetails from './nettests/Psiphon' import TorDetails from './nettests/Tor' +import VanillaTorDetails from './nettests/VanillaTor' import DefaultTestDetails from './nettests/Default' import TorSnowflakeDetails from './nettests/TorSnowflake' @@ -30,12 +29,12 @@ const mapTestDetails = { vanilla_tor: VanillaTorDetails, psiphon: PsiphonDetails, tor: TorDetails, - torsf: TorSnowflakeDetails + torsf: TorSnowflakeDetails, } - const MeasurementContainer = ({ testName, measurement, ...props }) => { - const TestDetails = testName in mapTestDetails ? mapTestDetails[testName] : DefaultTestDetails + const TestDetails = + testName in mapTestDetails ? mapTestDetails[testName] : DefaultTestDetails return ( <> @@ -45,7 +44,7 @@ const MeasurementContainer = ({ testName, measurement, ...props }) => { MeasurementContainer.propTypes = { measurement: PropTypes.any, - testName: PropTypes.any + testName: PropTypes.any, } export default MeasurementContainer diff --git a/components/measurement/PerformanceDetails.js b/components/measurement/PerformanceDetails.js index dd23bf182..db33718aa 100644 --- a/components/measurement/PerformanceDetails.js +++ b/components/measurement/PerformanceDetails.js @@ -1,4 +1,3 @@ -import React from 'react' import PropTypes from 'prop-types' import { useIntl } from 'react-intl' import { DetailsBoxTable } from './DetailsBox' @@ -10,47 +9,69 @@ const PerformanceDetails = ({ mss, packetLoss, outOfOrder, - timeouts + timeouts, }) => { const intl = useIntl() - let items = [] - averagePing && items.push({ - label: intl.formatMessage({ id: 'Measurement.Details.Performance.Label.AvgPing' }), - value: `${averagePing} ms` - }) - maxPing && items.push({ - label: intl.formatMessage({ id: 'Measurement.Details.Performance.Label.MaxPing' }), - value: `${isNdt7 ? '~' : ''}${maxPing} ms` - }) - mss && items.push({ - label: intl.formatMessage({ id: 'Measurement.Details.Performance.Label.MSS' }), - value: `${mss}` - }) + const items = [] + averagePing && + items.push({ + label: intl.formatMessage({ + id: 'Measurement.Details.Performance.Label.AvgPing', + }), + value: `${averagePing} ms`, + }) + maxPing && + items.push({ + label: intl.formatMessage({ + id: 'Measurement.Details.Performance.Label.MaxPing', + }), + value: `${isNdt7 ? '~' : ''}${maxPing} ms`, + }) + mss && + items.push({ + label: intl.formatMessage({ + id: 'Measurement.Details.Performance.Label.MSS', + }), + value: `${mss}`, + }) - packetLoss != undefined && items.push({ - label: isNdt7 ? intl.formatMessage({ - id: 'Measurement.Details.Performance.Label.RetransmitRate' - }): intl.formatMessage({ id: 'Measurement.Details.Performance.Label.PktLoss' }), - value: `${packetLoss}%` - }) + packetLoss !== undefined && + items.push({ + label: isNdt7 + ? intl.formatMessage({ + id: 'Measurement.Details.Performance.Label.RetransmitRate', + }) + : intl.formatMessage({ + id: 'Measurement.Details.Performance.Label.PktLoss', + }), + value: `${packetLoss}%`, + }) //Only add outOfOrder and timeouts if NDT4/5 measurement - if(!isNdt7) { - outOfOrder != undefined && items.push({ - label: intl.formatMessage({ id: 'Measurement.Details.Performance.Label.OutOfOrder' }), - value: outOfOrder.toString() + '%' - }) - timeouts != undefined && items.push({ - label: intl.formatMessage({ id: 'Measurement.Details.Performance.Label.Timeouts' }), - value: timeouts.toString() - }) + if (!isNdt7) { + outOfOrder !== undefined && + items.push({ + label: intl.formatMessage({ + id: 'Measurement.Details.Performance.Label.OutOfOrder', + }), + value: `${outOfOrder.toString()}%`, + }) + timeouts !== undefined && + items.push({ + label: intl.formatMessage({ + id: 'Measurement.Details.Performance.Label.Timeouts', + }), + value: timeouts.toString(), + }) } return ( ) } @@ -62,7 +83,7 @@ PerformanceDetails.propTypes = { mss: PropTypes.number.isRequired, packetLoss: PropTypes.number.isRequired, outOfOrder: PropTypes.number.isRequired, - timeouts: PropTypes.number.isRequired + timeouts: PropTypes.number.isRequired, } export default PerformanceDetails diff --git a/components/measurement/StatusInfo.js b/components/measurement/StatusInfo.js index eb341a75f..05e83c076 100644 --- a/components/measurement/StatusInfo.js +++ b/components/measurement/StatusInfo.js @@ -1,24 +1,18 @@ import PropTypes from 'prop-types' -import { - Flex, - Box, - Text -} from 'ooni-components' -const StatusInfo = ({ title, message}) => ( - - - {title} - - - {message} - - +const StatusInfo = ({ title, message }) => ( +
+
{title}
+
+ {' '} + {message}{' '} +
+
) StatusInfo.propTypes = { title: PropTypes.string, - message: PropTypes.string + message: PropTypes.string, } -export default StatusInfo \ No newline at end of file +export default StatusInfo diff --git a/components/measurement/SummaryText.js b/components/measurement/SummaryText.js index f6e1cdea6..1283fd1b6 100644 --- a/components/measurement/SummaryText.js +++ b/components/measurement/SummaryText.js @@ -1,61 +1,52 @@ -import React from 'react' import PropTypes from 'prop-types' -import { Flex, Text } from 'ooni-components' +import { useIntl } from 'react-intl' import dayjs from 'services/dayjs' - -import { getTestMetadata } from '../utils' import FormattedMarkdown from '../FormattedMarkdown' -import { useIntl } from 'react-intl' +import { getTestMetadata } from '../utils' -const SummaryText = ({ - testName, - network, - country, - date, - content, -}) => { +const SummaryText = ({ testName, network, country, date, content }) => { const { locale } = useIntl() const metadata = getTestMetadata(testName) - const formattedDateTime = dayjs(date).locale(locale).utc().format('MMMM DD, YYYY, hh:mm A [UTC]') + const formattedDateTime = dayjs(date) + .locale(locale) + .utc() + .format('MMMM DD, YYYY, hh:mm A [UTC]') let textToRender = null if (typeof content === 'function') { textToRender = content() } else if (typeof content === 'string') { - textToRender = - + ) } else { textToRender = content } return ( - - - {textToRender} - - +
+
{textToRender}
+
) } SummaryText.propTypes = { testName: PropTypes.string.isRequired, - network: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.number - ]), + network: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), country: PropTypes.string.isRequired, date: PropTypes.string.isRequired, content: PropTypes.oneOfType([ PropTypes.string, PropTypes.element, - PropTypes.func - ]) + PropTypes.func, + ]), } export default SummaryText diff --git a/components/measurement/nettests/Dash.js b/components/measurement/nettests/Dash.js index 20d7a8032..d7059019e 100644 --- a/components/measurement/nettests/Dash.js +++ b/components/measurement/nettests/Dash.js @@ -1,10 +1,6 @@ -import React from 'react' import PropTypes from 'prop-types' -import { Flex, Box } from 'ooni-components' -import { useIntl, defineMessages } from 'react-intl' - import { MdFlashOn } from 'react-icons/md' - +import { defineMessages, useIntl } from 'react-intl' import { InfoBoxItem } from '../InfoBoxItem' /* @@ -13,40 +9,40 @@ import { InfoBoxItem } from '../InfoBoxItem' */ const minimumBitrateForVideo = [ { - 'sfr_min_bitrate': 600, - 'hfr_min_bitrate': 1000, - 'type': '240p' + sfr_min_bitrate: 600, + hfr_min_bitrate: 1000, + type: '240p', }, { - 'sfr_min_bitrate': 1000, - 'hfr_min_bitrate': 1500, - 'type': '360p' + sfr_min_bitrate: 1000, + hfr_min_bitrate: 1500, + type: '360p', }, { - 'sfr_min_bitrate': 2500, - 'hfr_min_bitrate': 4000, - 'type': '480p' + sfr_min_bitrate: 2500, + hfr_min_bitrate: 4000, + type: '480p', }, { - 'sfr_min_bitrate': 5000, - 'hfr_min_bitrate': 7500, - 'type': '720p (HD)' + sfr_min_bitrate: 5000, + hfr_min_bitrate: 7500, + type: '720p (HD)', }, { - 'sfr_min_bitrate': 8000, - 'hfr_min_bitrate': 12000, - 'type': '1080p (full HD)' + sfr_min_bitrate: 8000, + hfr_min_bitrate: 12000, + type: '1080p (full HD)', }, { - 'sfr_min_bitrate': 16000, - 'hfr_min_bitrate': 24000, - 'type': '1440p (2k)' + sfr_min_bitrate: 16000, + hfr_min_bitrate: 24000, + type: '1440p (2k)', }, { - 'sfr_min_bitrate': 35000, - 'hfr_min_bitrate': 53000, - 'type': '2160p (4k)' - } + sfr_min_bitrate: 35000, + hfr_min_bitrate: 53000, + type: '2160p (4k)', + }, ] const getOptimalQualityForBitrate = (testKeys) => { @@ -64,8 +60,9 @@ const messages = defineMessages({ dash: { id: 'Measurement.Metadata.Dash', - defaultMessage: '{videoQuality} quality video streaming at {speed} Mbit/s speed in {country}' - } + defaultMessage: + '{videoQuality} quality video streaming at {speed} Mbit/s speed in {country}', + }, }) const DashDetails = ({ measurement, country, render }) => { @@ -73,49 +70,67 @@ const DashDetails = ({ measurement, country, render }) => { const testKeys = measurement.test_keys const failure = testKeys.failure - if (failure === true || typeof testKeys.simple === 'undefined' || typeof testKeys.receiver_data === 'undefined') { + if ( + failure === true || + typeof testKeys.simple === 'undefined' || + typeof testKeys.receiver_data === 'undefined' + ) { return render({ - status: 'error' + status: 'error', }) } const optimalVideoRate = getOptimalQualityForBitrate(testKeys).type - const medianBitrateInMbits = (testKeys.simple.median_bitrate / 1000).toFixed(2) - const playoutDelay = (testKeys.simple.min_playout_delay).toFixed(2) - - return ( - render({ - statusIcon: , - statusLabel: intl.formatMessage({ id: 'Measurement.Hero.Status.Dash.Title' }), - headMetadata: { - message: intl.formatMessage(messages.dash, {videoQuality: optimalVideoRate, speed: medianBitrateInMbits, country: country}), - formatted: true - }, - statusInfo: ( - - - - - - - - ), - details: null - }) + const medianBitrateInMbits = (testKeys.simple.median_bitrate / 1000).toFixed( + 2, ) + const playoutDelay = testKeys.simple.min_playout_delay.toFixed(2) + + return render({ + statusIcon: , + statusLabel: intl.formatMessage({ + id: 'Measurement.Hero.Status.Dash.Title', + }), + headMetadata: { + message: intl.formatMessage(messages.dash, { + videoQuality: optimalVideoRate, + speed: medianBitrateInMbits, + country: country, + }), + formatted: true, + }, + statusInfo: ( +
+
+ + + +
+
+ ), + details: null, + }) } DashDetails.propTypes = { - testKeys: PropTypes.object.isRequired + testKeys: PropTypes.object.isRequired, } export default DashDetails diff --git a/components/measurement/nettests/Default.js b/components/measurement/nettests/Default.js index 3eecd5eee..88f2c5884 100644 --- a/components/measurement/nettests/Default.js +++ b/components/measurement/nettests/Default.js @@ -1,11 +1,9 @@ import PropTypes from 'prop-types' -const DefaultTestDetails = ({ render }) => ( - render({}) -) +const DefaultTestDetails = ({ render }) => render({}) DefaultTestDetails.propTypes = { - render: PropTypes.func + render: PropTypes.func, } export default DefaultTestDetails diff --git a/components/measurement/nettests/FacebookMessenger.js b/components/measurement/nettests/FacebookMessenger.js index e33906da2..d3041ee96 100644 --- a/components/measurement/nettests/FacebookMessenger.js +++ b/components/measurement/nettests/FacebookMessenger.js @@ -1,31 +1,17 @@ -import React from 'react' import PropTypes from 'prop-types' -import { - Heading, - Container, - Text, - Flex, - Box -} from 'ooni-components' -import { defineMessages, FormattedMessage, useIntl } from 'react-intl' - -import { DetailsBox } from '../DetailsBox' +import { FormattedMessage, defineMessages, useIntl } from 'react-intl' import AccessPointStatus from '../AccessPointStatus' +import { DetailsBox } from '../DetailsBox' import StatusInfo from '../StatusInfo' export const FacebookMessengerDetails = ({ measurement, render }) => { const intl = useIntl() const testKeys = measurement.test_keys - const isWorking = ( + const isWorking = testKeys.facebook_dns_blocking === false && testKeys.facebook_tcp_blocking === false - ) const dnsBlocking = testKeys.facebook_dns_blocking === true const tcpBlocking = testKeys.facebook_tcp_blocking === true - const isFailed = ( - testKeys.facebook_dns_blocking === null && - testKeys.facebook_tcp_blocking === null - ) const tcpConnections = testKeys.tcp_connect const messages = defineMessages({ @@ -35,8 +21,8 @@ export const FacebookMessengerDetails = ({ measurement, render }) => { }, reachable: { id: 'Measurement.Metadata.FacebookMessenger.Reachable', - defaultMessage: 'Facebook Messenger was reachable in {country}', - } + defaultMessage: 'Facebook Messenger was reachable in {country}', + }, }) let summaryText = '' @@ -44,19 +30,35 @@ export const FacebookMessengerDetails = ({ measurement, render }) => { if (!isWorking) { if (tcpBlocking) { - summaryText += intl.formatMessage({id: 'Measurement.Details.SummaryText.FacebookMessenger.TCPFailure'}) - statusMessage.push(intl.formatMessage({id: 'Measurement.Details.FacebookMessenger.TCPFailed'})) + summaryText += intl.formatMessage({ + id: 'Measurement.Details.SummaryText.FacebookMessenger.TCPFailure', + }) + statusMessage.push( + intl.formatMessage({ + id: 'Measurement.Details.FacebookMessenger.TCPFailed', + }), + ) } else { - summaryText += intl.formatMessage({id: 'Measurement.Details.SummaryText.FacebookMessenger.TCPSuccess'}) + summaryText += intl.formatMessage({ + id: 'Measurement.Details.SummaryText.FacebookMessenger.TCPSuccess', + }) } summaryText += '' if (dnsBlocking) { - summaryText += intl.formatMessage({id: 'Measurement.Details.SummaryText.FacebookMessenger.DNSFailure'}) - statusMessage.push(intl.formatMessage({id: 'Measurement.Details.FacebookMessenger.DNSFailed'})) + summaryText += intl.formatMessage({ + id: 'Measurement.Details.SummaryText.FacebookMessenger.DNSFailure', + }) + statusMessage.push( + intl.formatMessage({ + id: 'Measurement.Details.FacebookMessenger.DNSFailed', + }), + ) } else { - summaryText += intl.formatMessage({id: 'Measurement.Details.SummaryText.FacebookMessenger.DNSSuccess'}) + summaryText += intl.formatMessage({ + id: 'Measurement.Details.SummaryText.FacebookMessenger.DNSSuccess', + }) } } else { summaryText = 'Measurement.Details.SummaryText.FacebookMessenger.Reachable' @@ -64,72 +66,94 @@ export const FacebookMessengerDetails = ({ measurement, render }) => { statusMessage = statusMessage.join('\n') - const statusTitle = isWorking ? - intl.formatMessage({id: 'Measurement.Status.Hint.FacebookMessenger.Reachable'}) : - intl.formatMessage({id: 'Measurement.Status.Hint.FacebookMessenger.Blocked'}) + const statusTitle = isWorking + ? intl.formatMessage({ + id: 'Measurement.Status.Hint.FacebookMessenger.Reachable', + }) + : intl.formatMessage({ + id: 'Measurement.Status.Hint.FacebookMessenger.Blocked', + }) - return ( - render({ - status: isWorking ? 'reachable' : 'anomaly', - statusInfo: , - summaryText: summaryText, - headMetadata: { - message: isWorking ? messages.reachable : messages.notReachable, - formatted: false - }, - details: ( - <> - - - } - ok={!dnsBlocking} - /> - - - } - ok={!tcpBlocking} - /> - - - } - content={ - <> - {Array.isArray(tcpConnections) && tcpConnections.length > 0 && + return render({ + status: isWorking ? 'reachable' : 'anomaly', + statusInfo: , + summaryText: summaryText, + headMetadata: { + message: isWorking ? messages.reachable : messages.notReachable, + formatted: false, + }, + details: ( + <> +
+
+ + } + ok={!dnsBlocking} + /> +
+
+ + } + ok={!tcpBlocking} + /> +
+
+ + } + content={ + <> + {Array.isArray(tcpConnections) && tcpConnections.length > 0 && ( <> {tcpConnections.map((connection, index) => ( - - - - {connection.status.failure && - {connection.ip}:{connection.port} }} +
+
+ {connection.status.failure && ( + + {' '} + {connection.ip}:{connection.port}{' '} + + ), + }} + /> + )} + {connection.status.success && ( + + {' '} + {connection.ip}:{connection.port}{' '} + + ), + }} /> - } - {connection.status.success && - {connection.ip}:{connection.port} }} - /> - } - - - + )} +
+
))} - } - - } - /> - - ) - }) - ) + )} + + } + /> + + ), + }) } FacebookMessengerDetails.propTypes = { measurement: PropTypes.object, - render: PropTypes.func + render: PropTypes.func, } export default FacebookMessengerDetails diff --git a/components/measurement/nettests/HTTPHeaderFieldManipulation.js b/components/measurement/nettests/HTTPHeaderFieldManipulation.js index 89ba629c9..09e7d8bc4 100644 --- a/components/measurement/nettests/HTTPHeaderFieldManipulation.js +++ b/components/measurement/nettests/HTTPHeaderFieldManipulation.js @@ -1,8 +1,4 @@ -import React from 'react' import PropTypes from 'prop-types' -import { - Text -} from 'ooni-components' import { FormattedMessage, defineMessages } from 'react-intl' export const HttpHeaderFieldManipulationDetails = ({ measurement, render }) => { @@ -22,33 +18,33 @@ export const HttpHeaderFieldManipulationDetails = ({ measurement, render }) => { const messages = defineMessages({ middleboxes: { id: 'Measurement.Metadata.HTTPHeaderManipulation.MiddleboxesDetected', - defaultMessage: 'HTTP header manipulation was detected in {country}' + defaultMessage: 'HTTP header manipulation was detected in {country}', }, noMiddleboxes: { id: 'Measurement.Metadata.HTTPHeaderManipulation.NoMiddleboxesDetected', - defaultMessage: 'HTTP header manipulation was not detected in {country}' - } + defaultMessage: 'HTTP header manipulation was not detected in {country}', + }, }) - return ( - render({ - status: isAnomaly ? 'anomaly' : 'reachable', - statusLabel: isAnomaly - ? - : , - summaryText: isAnomaly - ? 'Measurement.HTTPHeaderManipulation.MiddleBoxesDetected.SummaryText' - : 'Measurement.HTTPHeaderManipulation.NoMiddleBoxes.SummaryText', - headMetadata: { - message: isAnomaly ? messages.middleboxes : messages.noMiddleboxes, - formatted: false - } - }) - ) + return render({ + status: isAnomaly ? 'anomaly' : 'reachable', + statusLabel: isAnomaly ? ( + + ) : ( + + ), + summaryText: isAnomaly + ? 'Measurement.HTTPHeaderManipulation.MiddleBoxesDetected.SummaryText' + : 'Measurement.HTTPHeaderManipulation.NoMiddleBoxes.SummaryText', + headMetadata: { + message: isAnomaly ? messages.middleboxes : messages.noMiddleboxes, + formatted: false, + }, + }) } HttpHeaderFieldManipulationDetails.propTypes = { - testKeys: PropTypes.object + testKeys: PropTypes.object, } export default HttpHeaderFieldManipulationDetails diff --git a/components/measurement/nettests/HTTPInvalidRequestLine.js b/components/measurement/nettests/HTTPInvalidRequestLine.js index 3a9493d9e..69653f1c6 100644 --- a/components/measurement/nettests/HTTPInvalidRequestLine.js +++ b/components/measurement/nettests/HTTPInvalidRequestLine.js @@ -1,8 +1,4 @@ -import React from 'react' import PropTypes from 'prop-types' -import { - Text -} from 'ooni-components' import { FormattedMessage, defineMessages } from 'react-intl' const HttpInvalidRequestLineDetails = ({ measurement, render }) => { @@ -10,49 +6,49 @@ const HttpInvalidRequestLineDetails = ({ measurement, render }) => { const isAnomaly = testKeys.tampering === true const isOK = testKeys.tampering === false const isFailed = testKeys.tampering === null - const sentMessages = testKeys.sent const receivedMessages = testKeys.received const messages = defineMessages({ middleboxes: { id: 'Measurement.Metadata.HTTPInvalidReqLine.Middleboxes', - defaultMessage: 'Network traffic manipulation was detected in {country}' + defaultMessage: 'Network traffic manipulation was detected in {country}', }, noMiddleboxes: { id: 'Measurement.Metadata.HTTPInvalidReqLine.NoMiddleboxes', - defaultMessage: 'Network traffic manipulation was not detected in {country}' - } + defaultMessage: + 'Network traffic manipulation was not detected in {country}', + }, }) - return ( - render({ - status: isAnomaly ? 'anomaly' : 'reachable', - statusLabel: isAnomaly - ? - : , - summaryText: isAnomaly - ? 'Measurement.HTTPInvalidReqLine.MiddleboxesDetected.SummaryText' - : 'Measurement.HTTPInvalidReqLine.NoMiddleBoxes.SummaryText', - headMetadata: { - message: isAnomaly ? messages.middleboxes : messages.noMiddleboxes, - formatted: false - }, - details: ( -
- {/*isAnomaly: {isAnomaly.toString()} + return render({ + status: isAnomaly ? 'anomaly' : 'reachable', + statusLabel: isAnomaly ? ( + + ) : ( + + ), + summaryText: isAnomaly + ? 'Measurement.HTTPInvalidReqLine.MiddleboxesDetected.SummaryText' + : 'Measurement.HTTPInvalidReqLine.NoMiddleBoxes.SummaryText', + headMetadata: { + message: isAnomaly ? messages.middleboxes : messages.noMiddleboxes, + formatted: false, + }, + details: ( +
+ {/*isAnomaly: {isAnomaly.toString()} isOK: {isOK.toString()} isFailed: {isFailed.toString()} sentMessages: {sentMessages.toString()} receivedMessages: {receivedMessages.toString()}*/} -
- )} - ) - ) +
+ ), + }) } HttpInvalidRequestLineDetails.propTypes = { - testKeys: PropTypes.object + testKeys: PropTypes.object, } export default HttpInvalidRequestLineDetails diff --git a/components/measurement/nettests/Ndt.js b/components/measurement/nettests/Ndt.js index cfb072406..7839d3501 100644 --- a/components/measurement/nettests/Ndt.js +++ b/components/measurement/nettests/Ndt.js @@ -1,25 +1,14 @@ -import React from 'react' import PropTypes from 'prop-types' -import { - Flex, - Box, - Text -} from 'ooni-components' import { MdFlashOn } from 'react-icons/md' -import { useIntl, defineMessages } from 'react-intl' - -import { mlabServerDetails } from './mlab_utils' -import PerformanceDetails from '../PerformanceDetails' +import { defineMessages, useIntl } from 'react-intl' import { InfoBoxItem } from '../InfoBoxItem' +import PerformanceDetails from '../PerformanceDetails' +import { mlabServerDetails } from './mlab_utils' const ServerLocation = ({ serverAddress = '', isNdt7 }) => { const server = mlabServerDetails(serverAddress, isNdt7) - return ( - <> - {server ? `${server.city}, ${server.countryName}` : 'N/A'} - - ) + return <>{server ? `${server.city}, ${server.countryName}` : 'N/A'} } // NdtDetails is implemented differently for Ndt4/5 and for Ndt7 hence @@ -29,10 +18,14 @@ const NdtDetails = ({ measurement, render }) => { const testKeys = measurement.test_keys const isFailed = testKeys.failure !== null - const failure = testKeys.failure const isNdt7 = testKeys.protocol === 7 - let packetLoss, minRTT, maxRTT, mss, outOfOrder, timeouts + let packetLoss + let minRTT + let maxRTT + let mss + let outOfOrder + let timeouts const simple = (isNdt7 ? testKeys.summary : testKeys.simple) || {} @@ -40,7 +33,7 @@ const NdtDetails = ({ measurement, render }) => { // ex. use kbit/s if the speed is low and gbit/s if it's high const downloadMbit = simple.download && (simple.download / 1000).toFixed(2) const uploadMbit = simple.upload && (simple.upload / 1000).toFixed(2) - const ping = simple.ping && (simple.ping).toFixed(1) + const ping = simple.ping?.toFixed(1) let performanceDetails = null if (!isFailed) { @@ -48,21 +41,23 @@ const NdtDetails = ({ measurement, render }) => { if (isNdt7) { const summary = testKeys.summary || {} // Summary - packetLoss = summary.retransmit_rate && (summary.retransmit_rate * 100).toFixed(3) - minRTT = summary.min_rtt && (summary.min_rtt).toFixed(0) - maxRTT = summary.max_rtt && (summary.max_rtt).toFixed(0) + packetLoss = + summary.retransmit_rate && (summary.retransmit_rate * 100).toFixed(3) + minRTT = summary.min_rtt?.toFixed(0) + maxRTT = summary.max_rtt?.toFixed(0) mss = summary.mss outOfOrder = null timeouts = null - } - else { + } else { const advanced = testKeys.advanced || null // Advanced - delete advanced['out_of_order'] - packetLoss = advanced.packet_loss && (advanced.packet_loss * 100).toFixed(3) - outOfOrder = advanced.out_of_order && (advanced.out_of_order * 100).toFixed(1) - minRTT = advanced.min_rtt && (advanced.min_rtt).toFixed(0) - maxRTT = advanced.max_rtt && (advanced.max_rtt).toFixed(0) + advanced.out_of_order = undefined + packetLoss = + advanced.packet_loss && (advanced.packet_loss * 100).toFixed(3) + outOfOrder = + advanced.out_of_order && (advanced.out_of_order * 100).toFixed(1) + minRTT = advanced.min_rtt?.toFixed(0) + maxRTT = advanced.max_rtt?.toFixed(0) mss = advanced?.mss timeouts = advanced?.timeouts } @@ -87,52 +82,71 @@ const NdtDetails = ({ measurement, render }) => { const messages = defineMessages({ Ndt: { id: 'Measurement.Metadata.Ndt', - defaultMessage: 'Speed test result (NDT Test) in {country}' - } + defaultMessage: 'Speed test result (NDT Test) in {country}', + }, }) - // FIXME we need to style the failed test case properly - return ( - render({ - statusIcon: , - statusLabel: intl.formatMessage({id: 'Measurement.Hero.Status.NDT.Title'}), - headMetadata: { - message: messages.Ndt, - formatted: false - }, - statusInfo: ( - - {isFailed ? ( - -

Failed Test

-
- ) : ( - - - - - - } - /> - - )} -
- ), - details: ( -
{!isFailed && performanceDetails}
- ) - }) - ) + return render({ + statusIcon: , + statusLabel: intl.formatMessage({ + id: 'Measurement.Hero.Status.NDT.Title', + }), + headMetadata: { + message: messages.Ndt, + formatted: false, + }, + statusInfo: ( +
+ {isFailed ? ( +
+

Failed Test

+
+ ) : ( +
+ + + + + } + /> +
+ )} +
+ ), + details:
{!isFailed && performanceDetails}
, + }) } NdtDetails.propTypes = { - testKeys: PropTypes.object + testKeys: PropTypes.object, } export default NdtDetails diff --git a/components/measurement/nettests/Psiphon.js b/components/measurement/nettests/Psiphon.js index 640585542..3ab761cf6 100644 --- a/components/measurement/nettests/Psiphon.js +++ b/components/measurement/nettests/Psiphon.js @@ -1,35 +1,29 @@ -import React from 'react' import PropTypes from 'prop-types' -import { FormattedMessage } from 'react-intl' -import { Flex, Container } from 'ooni-components' import { MdTimer } from 'react-icons/md' -import { defineMessages } from 'react-intl' +import { FormattedMessage, defineMessages } from 'react-intl' import AccessPointStatus from '../AccessPointStatus' -const PsiphonDetails = ({ - measurement, - render -}) => { +const PsiphonDetails = ({ measurement, render }) => { const { - test_keys: { - failure, - bootstrap_time - } + test_keys: { failure, bootstrap_time }, } = measurement const messages = defineMessages({ reachable: { id: 'Measurement.Metadata.Psiphon.Reachable', - defaultMessage: 'Psiphon was reachable in {country}' + defaultMessage: 'Psiphon was reachable in {country}', }, unReachable: { id: 'Measurement.Metadata.Psiphon.UnReachable', - defaultMessage: 'Psiphon was NOT reachable in {country}' - } + defaultMessage: 'Psiphon was NOT reachable in {country}', + }, }) - let status, hint, summaryText, metaText + let status + let hint + let summaryText + let metaText // https://github.com/ooni/spec/blob/master/nettests/ts-015-psiphon.md#possible-conclusions // Determine if psiphon is blocked and if the probe could bootstrap psiphon @@ -38,16 +32,18 @@ const PsiphonDetails = ({ metaText = messages.unReachable if (bootstrap_time === 0) { // Unable to bootstrap - hint = + hint = ( + + ) summaryText = 'Measurement.Details.SummaryText.Psiphon.BootstrappingError' } else { // Unable to use Psiphon to reach https://google.com/humans.txt - hint = + hint = summaryText = 'Measurement.Details.SummaryText.Psiphon.Blocked' } } else { status = 'reachable' - hint = + hint = summaryText = 'Measurement.Details.SummaryText.Psiphon.OK' metaText = messages.reachable } @@ -60,25 +56,24 @@ const PsiphonDetails = ({ summaryText: summaryText, headMetadata: { message: metaText, - formatted: false + formatted: false, }, details: ( - <> - - - { - bootstrap_time && - } - label={} - content={Number(bootstrap_time).toFixed(2)} - ok={true} - /> - } - - - - ) +
+
+ {bootstrap_time && ( + } + label={ + + } + content={Number(bootstrap_time).toFixed(2)} + ok={true} + /> + )} +
+
+ ), })} ) @@ -87,7 +82,7 @@ const PsiphonDetails = ({ PsiphonDetails.propTypes = { render: PropTypes.func, measurement: PropTypes.object.isRequired, - country: PropTypes.string.isRequired + country: PropTypes.string.isRequired, } export default PsiphonDetails diff --git a/components/measurement/nettests/Signal.js b/components/measurement/nettests/Signal.js index 5dc112044..20c6d5660 100644 --- a/components/measurement/nettests/Signal.js +++ b/components/measurement/nettests/Signal.js @@ -1,33 +1,30 @@ -import React from 'react' import PropTypes from 'prop-types' import { defineMessages, useIntl } from 'react-intl' const messages = defineMessages({ 'blockingReason.dns_nxdomain_error': { id: 'Measurement.SummaryText.Signal.Anomaly.BlockingReason.dns_nxdomain_error', - defaultMessage: 'DNS Tampering' + defaultMessage: 'DNS Tampering', }, metaReachable: { id: 'Measurement.Metadata.Telegram.Reachable', - defaultMessage: 'Signal was reachable in {country}' + defaultMessage: 'Signal was reachable in {country}', }, metaBlocked: { id: 'Measurement.Metadata.Telegram.Blocked', - defaultMessage: 'Signal was NOT reachable in {country}' - } + defaultMessage: 'Signal was NOT reachable in {country}', + }, }) const SignalDetails = ({ measurement, render }) => { const intl = useIntl() const testKeys = measurement.test_keys - const { - signal_backend_status, - signal_backend_failure, - } = testKeys + const { signal_backend_status, signal_backend_failure } = testKeys const anomaly = signal_backend_status === 'blocked' - const blockingReason = signal_backend_failure - let hint = intl.formatMessage({ id: 'Measurement.Status.Hint.Signal.Reachable' }) + let hint = intl.formatMessage({ + id: 'Measurement.Status.Hint.Signal.Reachable', + }) let summaryText = 'Measurement.Details.SummaryText.Signal.Reachable' let headMetadata = messages.metaReachable @@ -37,22 +34,20 @@ const SignalDetails = ({ measurement, render }) => { headMetadata = messages.metaBlocked } - return ( - render({ - status: anomaly ? 'anomaly' : 'reachable', - statusInfo: hint, - summaryText: summaryText, - headMetadata: { - message: headMetadata, - formatted: false - } - }) - ) + return render({ + status: anomaly ? 'anomaly' : 'reachable', + statusInfo: hint, + summaryText: summaryText, + headMetadata: { + message: headMetadata, + formatted: false, + }, + }) } SignalDetails.propTypes = { measurement: PropTypes.object.isRequired, - render: PropTypes.func + render: PropTypes.func, } export default SignalDetails diff --git a/components/measurement/nettests/Telegram.js b/components/measurement/nettests/Telegram.js index 59cbd0a99..10d1281d0 100644 --- a/components/measurement/nettests/Telegram.js +++ b/components/measurement/nettests/Telegram.js @@ -1,17 +1,6 @@ -import React from 'react' import PropTypes from 'prop-types' -import { - Container, - Heading, - Text, - Flex, - Box, -} from 'ooni-components' -import { FormattedMessage } from 'react-intl' -import { MdPhoneAndroid } from 'react-icons/md' -import { MdWebAsset } from 'react-icons/md' -import { defineMessages } from 'react-intl' - +import { MdPhoneAndroid, MdWebAsset } from 'react-icons/md' +import { FormattedMessage, defineMessages } from 'react-intl' import AccessPointStatus from '../AccessPointStatus' import { DetailsBox } from '../DetailsBox' @@ -21,24 +10,26 @@ const TelegramDetails = ({ measurement, render }) => { telegram_web_status, telegram_tcp_blocking, telegram_http_blocking, - tcp_connect + tcp_connect, } = testKeys const message = defineMessages({ reachable: { id: 'Measurement.Metadata.Telegram.Reachable', - defaultMessage: 'Telegram was reachable in {country}' + defaultMessage: 'Telegram was reachable in {country}', }, unReachable: { id: 'Measurement.Metadata.Telegram.UnReachable', - defaultMessage: 'Telegram was NOT reachable in {country}' - } + defaultMessage: 'Telegram was NOT reachable in {country}', + }, }) let telegramWebOK = true let telegramDesktopOK = true let anomaly = false - let hint = + let hint = ( + + ) let summaryText = 'Measurement.Details.SummaryText.Telegram.Reachable' let headMetadata = message.reachable @@ -56,72 +47,91 @@ const TelegramDetails = ({ measurement, render }) => { if (!telegramWebOK || !telegramDesktopOK) { anomaly = true - hint = - summaryText = 'Measurement.Details.SummaryText.Telegram.DesktopAndAppFailure' + hint = + summaryText = + 'Measurement.Details.SummaryText.Telegram.DesktopAndAppFailure' headMetadata = message.unReachable } - return ( - render({ - status: anomaly ? 'anomaly': 'reachable', - statusInfo: hint, - summaryText: summaryText, - headMetadata: { - message: headMetadata, - formatted: false - }, - details: ( - <> - - - } - label={} - ok={telegramDesktopOK} - /> - - - } - label={} - ok={telegramWebOK} - /> - - - {Array.isArray(tcp_connect) && tcp_connect.length > 0 && - } - collapsed={false} - > - {tcp_connect.map((connection, index) => ( - - - - {connection.status.failure && - {connection.ip}:{connection.port} }} - /> - } - {connection.status.success && - {connection.ip}:{connection.port} }} - /> - } - - - - ))} - - } - - ) - }) - ) + return render({ + status: anomaly ? 'anomaly' : 'reachable', + statusInfo: hint, + summaryText: summaryText, + headMetadata: { + message: headMetadata, + formatted: false, + }, + details: ( + <> +
+
+ } + label={ + + } + ok={telegramDesktopOK} + /> +
+
+ } + label={ + + } + ok={telegramWebOK} + /> +
+
+ {Array.isArray(tcp_connect) && tcp_connect.length > 0 && ( + + } + collapsed={false} + > + {tcp_connect.map((connection, index) => ( +
+
+ {connection.status.failure && ( + + {' '} + {connection.ip}:{connection.port}{' '} + + ), + }} + /> + )} + {connection.status.success && ( + + {' '} + {connection.ip}:{connection.port}{' '} + + ), + }} + /> + )} +
+
+ ))} +
+ )} + + ), + }) } TelegramDetails.propTypes = { measurement: PropTypes.object.isRequired, - render: PropTypes.func + render: PropTypes.func, } export default TelegramDetails diff --git a/components/measurement/nettests/Tor.js b/components/measurement/nettests/Tor.js index 829a6c203..54d6c34f8 100644 --- a/components/measurement/nettests/Tor.js +++ b/components/measurement/nettests/Tor.js @@ -1,114 +1,71 @@ -import { Flex, Text, theme } from 'ooni-components' +import { colors } from 'ooni-components' import { Cross, Tick } from 'ooni-components/icons' import PropTypes from 'prop-types' -import React, { useMemo } from 'react' +import { useMemo } from 'react' import { FaClipboard } from 'react-icons/fa' import { FormattedMessage, defineMessages } from 'react-intl' import { useSortBy, useTable } from 'react-table' -import styled from 'styled-components' import { useClipboard } from 'use-clipboard-copy' +import { twMerge } from 'tailwind-merge' import AccessPointStatus from '../AccessPointStatus' -const TableStyles = styled.div` - table { - border-spacing: 0; - border: 1px solid black; - table-layout: fixed; - word-break: break-all; - width: 100%; - tr { - :last-child { - td { - border-bottom: 0; - } - } - } - th, - td { - margin: 0; - padding: 0.5rem; - border-bottom: 1px solid black; - text-align: center; - :last-child { - border-right: 0; - } - } - th { - border-right: 1px solid black; - } - } -` - -const StyledNameCell = styled.div` - overflow: hidden; - text-overflow: ellipsis; - position: relative; -` - -const ClipboardIcon = styled.div` - position: absolute; - right: -3px; - top: -5px; -` - const NameCell = ({ children }) => { const clipboard = useClipboard({ copiedTimeout: 1500 }) return ( <> - clipboard.copy(children)} > {children} - - {clipboard.copied && } - - - +
+ {clipboard.copied && ( + + )} +
+
) } NameCell.propTypes = { - children: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.element - ]).isRequired + children: PropTypes.oneOfType([PropTypes.string, PropTypes.element]) + .isRequired, } const Table = ({ columns, data }) => { - const { - getTableProps, - getTableBodyProps, - headerGroups, - rows, - prepareRow - } = useTable( - { - columns, - data - }, - useSortBy - ) + const { getTableProps, getTableBodyProps, headerGroups, rows, prepareRow } = + useTable( + { + columns, + data, + }, + useSortBy, + ) + const cellClassNames = 'm-0 p-2 border-b border-black text-center' return ( /* eslint-disable react/jsx-key */ - +
- {headerGroups.map(headerGroup => ( - - {headerGroup.headers.map(column => ( - + {headerGroup.headers.map((column, j) => ( + ))} @@ -120,25 +77,33 @@ const Table = ({ columns, data }) => { prepareRow(row) return ( - {row.cells.map(cell => { - return + {row.cells.map((cell) => { + return ( + + ) })} - )} - )} + ) + })}
+ {headerGroups.map((headerGroup, i) => ( +
{column.render('Header')} {/* Sort order indicator */} - {column.isSorted - ? column.isSortedDesc - ? ' 🔽' - : ' 🔼' - : ''} + {column.isSorted ? (column.isSortedDesc ? ' 🔽' : ' 🔼') : ''}
{cell.render('Cell')} + {cell.render('Cell')} +
/* eslint-enable react/jsx-key */ - ) } -const ConnectionStatusCell = ({ cell: { value} }) => { +const ConnectionStatusCell = ({ cell: { value } }) => { let statusIcon = null if (value === false) { - statusIcon = N/A + statusIcon =
N/A
} else { - statusIcon = value === null ? : + statusIcon = + value === null ? ( + + ) : ( + + ) } return ( <> @@ -147,26 +112,23 @@ const ConnectionStatusCell = ({ cell: { value} }) => { ) } -const TorDetails = ({ - isAnomaly, - isFailure, - measurement, - render -}) => { +const TorDetails = ({ isAnomaly, isFailure, measurement, render }) => { // https://github.com/ooni/spec/blob/master/nettests/ts-023-tor.md#possible-conclusions - let status, hint, summaryText + let status + let hint + let summaryText if (isFailure) { status = 'error' - hint = + hint = summaryText = 'Measurement.Details.SummaryText.Tor.Error' } else if (isAnomaly) { status = 'anomaly' - hint = + hint = summaryText = 'Measurement.Details.SummaryText.Tor.Blocked' } else { status = 'reachable' - hint = + hint = summaryText = 'Measurement.Details.SummaryText.Tor.OK' } @@ -176,49 +138,60 @@ const TorDetails = ({ or_port_dirauth_total, obfs4_accessible, obfs4_total, - targets = {} + targets = {}, } = testKeys - const columns = useMemo(() => [ - { - Header: , - accessor: 'name', - Cell: ({ cell: { value } }) => ( // eslint-disable-line react/display-name,react/prop-types - {value} - ) - }, - { - Header: , - accessor: 'address' - }, - { - Header: , - accessor: 'type' - }, - { - Header: , - accessor: 'failure', - collapse: true, - Cell: ConnectionStatusCell - }, - ], []) + const columns = useMemo( + () => [ + { + Header: ( + + ), + accessor: 'name', + Cell: ( + { cell: { value } }, // eslint-disable-line react/display-name,react/prop-types + ) => {value}, + }, + { + Header: ( + + ), + accessor: 'address', + }, + { + Header: ( + + ), + accessor: 'type', + }, + { + Header: , + accessor: 'failure', + collapse: true, + Cell: ConnectionStatusCell, + }, + ], + [], + ) - const data = useMemo(() => ( - Object.keys(targets).map(target => { - return { - name: targets[target].target_name || target, - address: targets[target].target_address, - type: targets[target].target_protocol, - failure: targets[target].failure, - } - }) - ), [targets]) + const data = useMemo( + () => + Object.keys(targets).map((target) => { + return { + name: targets[target].target_name || target, + address: targets[target].target_address, + type: targets[target].target_protocol, + failure: targets[target].failure, + } + }), + [targets], + ) const messages = defineMessages({ tor: { id: 'Measurement.Metadata.Tor', - defaultMessage: 'Tor censorship test result in {country}' - } + defaultMessage: 'Tor censorship test result in {country}', + }, }) return ( @@ -229,56 +202,56 @@ const TorDetails = ({ summaryText: summaryText, headMetadata: { message: messages.tor, - formatted: false + formatted: false, }, details: ( <> - {!!Object.keys(testKeys).length && + {!!Object.keys(testKeys).length && ( <> - +
} + width={1 / 2} + label={ + + } content={ } ok={true} - color='blue5' + color="blue5" /> } + width={1 / 2} + label={ + + } content={ } ok={true} - color='blue5' - /> - - - - + + +
- } + )} - ) + ), })} ) @@ -287,7 +260,7 @@ const TorDetails = ({ TorDetails.propTypes = { render: PropTypes.func, measurement: PropTypes.object.isRequired, - country: PropTypes.string.isRequired + country: PropTypes.string.isRequired, } export default TorDetails diff --git a/components/measurement/nettests/TorSnowflake.js b/components/measurement/nettests/TorSnowflake.js index 5b02174d5..a607e3280 100644 --- a/components/measurement/nettests/TorSnowflake.js +++ b/components/measurement/nettests/TorSnowflake.js @@ -1,49 +1,51 @@ -import React from 'react' import PropTypes from 'prop-types' -import { FormattedMessage, defineMessages } from 'react-intl' -import { Flex, Container } from 'ooni-components' import { MdTimer } from 'react-icons/md' +import { FormattedMessage, defineMessages } from 'react-intl' import AccessPointStatus from '../AccessPointStatus' const messages = defineMessages({ reachable: { id: 'Measurement.Metadata.TorSnowflake.Reachable', - defaultMessage: '' + defaultMessage: '', }, unReachable: { id: 'Measurement.Metadata.TorSnowflake.UnReachable', - defaultMessage: '' + defaultMessage: '', }, error: { id: 'Measurement.Metadata.TorSnowflake.Error', - defaultMessage: '' - } + defaultMessage: '', + }, }) const TorSnowflakeDetails = ({ isAnomaly, isFailure, measurement, render }) => { - const { - test_keys: { - bootstrap_time, - failure - } + const { + test_keys: { bootstrap_time, failure }, } = measurement - let status, hint, summaryText, metaText + let status + let hint + let summaryText + let metaText if (isFailure) { status = 'error' - hint = + hint = summaryText = 'Measurement.Details.SummaryText.TorSnowflake.Error' metaText = messages.error } else if (isAnomaly) { status = 'anomaly' - hint = + hint = ( + + ) summaryText = 'Measurement.Details.SummaryText.TorSnowflake.Blocked' metaText = messages.unReachable } else { status = 'reachable' - hint = + hint = ( + + ) summaryText = 'Measurement.Details.SummaryText.TorSnowflake.OK' metaText = messages.reachable } @@ -56,32 +58,33 @@ const TorSnowflakeDetails = ({ isAnomaly, isFailure, measurement, render }) => { summaryText: summaryText, headMetadata: { message: metaText, - formatted: false + formatted: false, }, details: ( - <> - - - { isAnomaly && - } - content={failure} - ok={true} - /> - } - { - !isAnomaly && !isFailure && bootstrap_time !== null && - } - label={} - content={Number(bootstrap_time).toFixed(2)} - ok={true} - /> - } - - - - ) +
+
+ {isAnomaly && ( + + } + content={failure} + ok={true} + /> + )} + {!isAnomaly && !isFailure && bootstrap_time !== null && ( + } + label={ + + } + content={Number(bootstrap_time).toFixed(2)} + ok={true} + /> + )} +
+
+ ), })} ) @@ -89,7 +92,7 @@ const TorSnowflakeDetails = ({ isAnomaly, isFailure, measurement, render }) => { TorSnowflakeDetails.propTypes = { render: PropTypes.func, - measurement: PropTypes.object.isRequired + measurement: PropTypes.object.isRequired, } -export default TorSnowflakeDetails \ No newline at end of file +export default TorSnowflakeDetails diff --git a/components/measurement/nettests/VanillaTor.js b/components/measurement/nettests/VanillaTor.js index ba4d8f7de..78467f574 100644 --- a/components/measurement/nettests/VanillaTor.js +++ b/components/measurement/nettests/VanillaTor.js @@ -1,12 +1,6 @@ -import React from 'react' import PropTypes from 'prop-types' -import { - Container, - Flex, -} from 'ooni-components' -import { FormattedMessage, defineMessages } from 'react-intl' import { MdCheckCircle } from 'react-icons/md' -import { MdTimelapse } from 'react-icons/md' +import { FormattedMessage, defineMessages } from 'react-intl' import AccessPointStatus from '../AccessPointStatus' @@ -14,57 +8,57 @@ const VanillaTorDetails = ({ measurement, render, isAnomaly, isFailure }) => { const messages = defineMessages({ reachable: { id: 'Measurement.Metadata.VanillaTor.Reachable', - defaultMessage: 'Vanilla Tor was able to bootstrap in {country}' + defaultMessage: 'Vanilla Tor was able to bootstrap in {country}', }, unReachable: { id: 'Measurement.Metadata.VanillaTor.UnReachable', - defaultMessage: 'Vanilla Tor was NOT able to bootstrap in {country}' - } + defaultMessage: 'Vanilla Tor was NOT able to bootstrap in {country}', + }, }) - return ( - render({ - status: isAnomaly ? 'anomaly' : 'reachable', - statusLabel: isAnomaly - ? - : , - summaryText: isAnomaly + return render({ + status: isAnomaly ? 'anomaly' : 'reachable', + statusLabel: isAnomaly ? ( + + ) : ( + + ), + summaryText: isAnomaly ? 'Measurement.Details.SummaryText.TorVanilla.Blocked' : 'Measurement.Details.SummaryText.TorVanilla.Reachable', - headMetadata: { - message: isAnomaly ? messages.unReachable : messages.reachable, - formatted: false - }, - details: ( - <> - - - } - label={} - ok={!isAnomaly} - /> - {/* } - label={} - ok={!isAnomaly} - content={} - /> */} - - - - )} - ) - ) + headMetadata: { + message: isAnomaly ? messages.unReachable : messages.reachable, + formatted: false, + }, + details: ( +
+
+ } + label={ + + } + ok={!isAnomaly} + /> + {/* } + label={} + ok={!isAnomaly} + content={} + /> */} +
+
+ ), + }) } VanillaTorDetails.propTypes = { measurement: PropTypes.object.isRequired, isAnomaly: PropTypes.bool.isRequired, isFailure: PropTypes.bool.isRequired, - render: PropTypes.func + render: PropTypes.func, } export default VanillaTorDetails diff --git a/components/measurement/nettests/WebConnectivity.js b/components/measurement/nettests/WebConnectivity.js index 0c590506e..335a3de61 100644 --- a/components/measurement/nettests/WebConnectivity.js +++ b/components/measurement/nettests/WebConnectivity.js @@ -1,148 +1,127 @@ import bufferFrom from 'buffer-from' -import NLink from 'next/link' -import { - Box, - Flex, - Heading, - Text -} from 'ooni-components' -import PropTypes from 'prop-types' -import React from 'react' -import url from 'url' - import deepmerge from 'deepmerge' +import Link from 'next/link' import { Cross, Tick } from 'ooni-components/icons' -import styled from 'styled-components' +import PropTypes from 'prop-types' +import url from 'url' import { FormattedMessage, defineMessages, useIntl } from 'react-intl' +import { Fragment } from 'react' import { DetailsBox } from '../DetailsBox' import StatusInfo from '../StatusInfo' const messages = defineMessages({ 'blockingReason.http-diff': { id: 'Measurement.SummaryText.Websites.Anomaly.BlockingReason.HTTP-diff', - defaultMessage: '' + defaultMessage: '', }, 'blockingReason.http-failure': { id: 'Measurement.SummaryText.Websites.Anomaly.BlockingReason.HTTP-failure', - defaultMessage: '' + defaultMessage: '', }, 'blockingReason.dns': { id: 'Measurement.SummaryText.Websites.Anomaly.BlockingReason.DNS', - defaultMessage: '' + defaultMessage: '', }, 'blockingReason.tcp_ip': { id: 'Measurement.SummaryText.Websites.Anomaly.BlockingReason.TCP', - defaultMessage: '' + defaultMessage: '', }, }) -// From https://css-tricks.com/snippets/css/make-pre-text-wrap/ -const WrappedPre = styled.pre` - white-space: pre-wrap; /* css-3 */ - white-space: -moz-pre-wrap; /* Mozilla, since 1999 */ - white-space: -pre-wrap; /* Opera 4-6 */ - white-space: -o-pre-wrap; /* Opera 7 */ - word-wrap: break-word; /* Internet Explorer 5.5+ */ -` - -const HttpResponseBodyContainer = styled(WrappedPre)` - max-height: 500px; - overflow: auto; -` - -const HttpResponseBody = ({request}) => { +const HttpResponseBody = ({ request }) => { let body if (!request || !request.response || !request.response.body) { return

Empty body

} body = request.response.body - if (typeof body == 'object' && body.format === 'base64') { + if (typeof body === 'object' && body.format === 'base64') { body = bufferFrom(body.data, 'base64').toString('binary') } - return ( - - {body} - - ) + return
{body}
} HttpResponseBody.propTypes = { - request: PropTypes.object.isRequired + request: PropTypes.object.isRequired, } -const RequestResponseContainer = ({request}) => { +const RequestResponseContainer = ({ request }) => { return ( // FIXME: This sometime ends up creating empty sections with just a title // when request data contains states like 'generic_timeout_error' // e.g ?report_id=20180709T222326Z_AS37594_FFQFSoqLJWYMgU0EnSbIK7PxicwJTFenIz9PupZYZWoXwtpCTy request.failure ? ( - - - +
+ +
) : ( - // !request.failure && - - + // !request.failure && + <> +
{/* Request URL */} - - - - -
{request.request.method} {request.request.url}
-
+
+
+ +
+
+
+
+              {request.request.method} {request.request.url}
+            
+
{/* Response Headers */} - - - - - - { - request.response.headers ? - Object.keys(request.response.headers).map((header, index) => ( - - - - {header}: - - - {request.response.headers[header]} - - - - )) : +
+
+ +
+
+
+
+              {request.response.headers ? (
+                Object.keys(request.response.headers).map((header, index) => (
+                  
+                    
+
{header}:
+
{request.response.headers[header]}
+
+
+ )) + ) : ( <>Empty headers - } - - + )} +
+
{/* Response Body (HTML) */} - - - - +
+
+ +
+
+
- - - +
+
+ ) ) } RequestResponseContainer.propTypes = { - request: PropTypes.object.isRequired + request: PropTypes.object.isRequired, } -const FailureString = ({failure}) => { +const FailureString = ({ failure }) => { if (typeof failure === 'undefined') { - return () + return } if (!failure) { return (
- + {' '} +
) } @@ -155,39 +134,43 @@ const FailureString = ({failure}) => { } FailureString.propTypes = { - failure: PropTypes.string + failure: PropTypes.string, } const DnsNarrowAnswerCell = (props) => ( - {props.children} +
{props.children}
) -const DnsAnswerCell = (props) => ( - {props.children} -) +const DnsAnswerCell = (props) =>
{props.children}
DnsAnswerCell.propTypes = { - children: PropTypes.any + children: PropTypes.any, } const dnsAnswerIpInfo = (dnsAnswer) => { - const asn = dnsAnswer.asn ? `AS${dnsAnswer.asn}` : 'Unknown AS' - const asOrgName = dnsAnswer.as_org_name ? `(${dnsAnswer.as_org_name})` : '' + const asn = dnsAnswer.asn ? `AS${dnsAnswer.asn}` : 'Unknown AS' + const asOrgName = dnsAnswer.as_org_name ? `(${dnsAnswer.as_org_name})` : '' - return `${asn} ${asOrgName}`.trim() + return `${asn} ${asOrgName}`.trim() } -const DnsAnswerRow = ({ name = 'Name', netClass = 'Class', ttl = 'TTL', type = 'Type', data = 'DATA', answer_ip_info = 'Answer IP Info', header = false}) => ( - - - {name} - {netClass} - {ttl} - {type} - {data} - {answer_ip_info} - - +const DnsAnswerRow = ({ + name = 'Name', + netClass = 'Class', + ttl = 'TTL', + type = 'Type', + data = 'DATA', + answer_ip_info = 'Answer IP Info', + header = false, +}) => ( +
+ {name} + {netClass} + {ttl} + {type} + {data} + {answer_ip_info} +
) DnsAnswerRow.propTypes = { @@ -197,69 +180,66 @@ DnsAnswerRow.propTypes = { type: PropTypes.string, data: PropTypes.string, answer_ip_info: PropTypes.string, - header: PropTypes.bool + header: PropTypes.bool, } -const QueryContainer = ({query}) => { - const { - query_type, - answers, - hostname, - engine, - failure - } = query +const QueryContainer = ({ query }) => { + const { query_type, answers, hostname, engine, failure } = query return ( - - +
+
{/* Metadata */} - - +
+
Query: - - +
+
IN {query_type} {hostname} - - - - +
+
+
+
Engine: - - - {engine} - - - - {failure && } - {!failure && - +
+
{engine}
+
+
+ {failure && ( +
+ +
+ )} + {!failure && ( +
- {Array.isArray(answers) && answers.map((dnsAnswer, index) => ( - - ))} - - } - - + {Array.isArray(answers) && + answers.map((dnsAnswer, index) => ( + + ))} +
+ )} +
) } QueryContainer.propTypes = { - query: PropTypes.object + query: PropTypes.object, } /* @@ -277,8 +257,8 @@ const validateMeasurement = (measurement) => { probe_asn: undefined, scores: { analysis: { - blocking_type: undefined - } + blocking_type: undefined, + }, }, test_keys: { accessible: undefined, @@ -289,26 +269,20 @@ const validateMeasurement = (measurement) => { client_resolver: undefined, http_experiment_failure: undefined, dns_experiment_failure: undefined, - control_failure: undefined - } + control_failure: undefined, + }, } return deepmerge(validDefaults, measurement) } -const getSearchHref = (input) => (`${process.env.NEXT_PUBLIC_EXPLORER_URL}/search?input=${input}`) +const getSearchHref = (input) => + `${process.env.NEXT_PUBLIC_EXPLORER_URL}/search?input=${input}` -const StyledLink = styled(NLink)` - direction: ltr; - display: inline; -` - -const StyledStatusInfoLink = styled(NLink)` - color: white; - text-decoration: underline; - &:hover { - color: white; - } -` +const StyledLink = ({ children, href }) => ( + + {children} + +) const WebConnectivityDetails = ({ isConfirmed, @@ -320,7 +294,7 @@ const WebConnectivityDetails = ({ measurement_start_time, probe_asn, input, - render + render, }) => { const { test_keys: { @@ -332,277 +306,332 @@ const WebConnectivityDetails = ({ client_resolver, http_experiment_failure, dns_experiment_failure, - control_failure - } + control_failure, + }, } = validateMeasurement(measurement ?? {}) const intl = useIntl() - const date = new Intl.DateTimeFormat(intl.locale, { dateStyle: 'long', timeStyle: 'long', timeZone: 'UTC' }).format(new Date(measurement_start_time)) - + const date = new Intl.DateTimeFormat(intl.locale, { + dateStyle: 'long', + timeStyle: 'long', + timeZone: 'UTC', + }).format(new Date(measurement_start_time)) + const p = url.parse(input) const hostname = p.host let status = 'default' let reason = null let summaryText = '' - let headMetadata = { message: '', formatted: true } + const headMetadata = { message: '', formatted: true } if (isFailure) { status = 'error' reason = null - summaryText = {input}, - network: probe_asn, - country - }} - /> - } else if(isConfirmed) { + summaryText = ( + {input} + ), + network: probe_asn, + country, + }} + /> + ) + } else if (isConfirmed) { status = 'confirmed' - summaryText = {input}, - network: probe_asn, - country - }} - /> + summaryText = ( + {input} + ), + network: probe_asn, + country, + }} + /> + ) headMetadata.message = intl.formatMessage( { id: 'Measurement.Metadata.WebConnectivity.ConfirmedBlocked', - defaultMessage: '{hostname} was blocked in {country}' + defaultMessage: '{hostname} was blocked in {country}', }, { date, hostname, country, - } + }, ) } else if (isAnomaly) { status = 'anomaly' const blockingReason = blocking ?? scores?.analysis?.blocking_type ?? null - reason = messages[`blockingReason.${blockingReason}`] && intl.formatMessage(messages[`blockingReason.${blockingReason}`]) - summaryText = {input}, - 'link-to-docs': (string) => ({string}), - network: probe_asn, - country, - reason - }} - /> + reason = + messages[`blockingReason.${blockingReason}`] && + intl.formatMessage(messages[`blockingReason.${blockingReason}`]) + summaryText = ( + {input} + ), + 'link-to-docs': (string) => ( + + {string} + + ), + network: probe_asn, + country, + reason, + }} + /> + ) headMetadata.message = intl.formatMessage( { id: 'Measurement.Metadata.WebConnectivity.Anomaly', - defaultMessage: '{hostname} showed signs of {reason} in {country}' + defaultMessage: '{hostname} showed signs of {reason} in {country}', }, { date, hostname, country, - reason - } + reason, + }, ) } else if (accessible) { status = 'reachable' - summaryText = {input}, - network: probe_asn, - country - }} - /> + summaryText = ( + {input} + ), + network: probe_asn, + country, + }} + /> + ) headMetadata.message = intl.formatMessage( { id: 'Measurement.Metadata.WebConnectivity.Accessible', - defaultMessage: '{hostname} was accessible in {country}' + defaultMessage: '{hostname} was accessible in {country}', }, { date, hostname, country, - } + }, ) } else if (blocking === false) { // When not accessible, but also not blocking, it must be down status = 'down' - summaryText = {input}, - network: probe_asn, - country - }} - /> + summaryText = ( + {input} + ), + network: probe_asn, + country, + }} + /> + ) headMetadata.message = intl.formatMessage( { id: 'Measurement.Metadata.WebConnectivity.Down', - defaultMessage: '{hostname} was down in {country}' + defaultMessage: '{hostname} was down in {country}', }, { date, hostname, country, - } + }, ) } else { // Fallback condition to handle older measurements not present in fastpath // See: https://github.com/ooni/explorer/issues/426#issuecomment-612094244 status = 'error' - summaryText = {input}, - network: probe_asn, - country - }} - /> + summaryText = ( + {input} + ), + network: probe_asn, + country, + }} + /> + ) headMetadata.message = intl.formatMessage( { id: 'Measurement.Metadata.WebConnectivity.Failed', - defaultMessage: '{hostname} failed to be measured in {country}' + defaultMessage: '{hostname} failed to be measured in {country}', }, { date, hostname, country, - } + }, ) } - const tcpConnections = Array.isArray(tcp_connect) ? tcp_connect.map((connection) => { - return { - destination: connection.ip + ':' + connection.port, - failure: connection.status?.failure - } - }) : [] + const tcpConnections = Array.isArray(tcp_connect) + ? tcp_connect.map((connection) => { + return { + destination: `${connection.ip}:${connection.port}`, + failure: connection.status?.failure, + } + }) + : [] return ( <> {render({ status: status, - statusInfo: - {input} - - } - message={reason} - />, + statusInfo: ( + + {input} + + } + message={reason} + /> + ), summaryText: summaryText, headMetadata: headMetadata, details: ( <> {/* Failures */} - +
} + title={ + + } content={ - - - - - +
+
+ +
+
- - - - - +
+
+ +
+
- - - - - +
+
+ +
+
- - +
+
} /> -
+
{/* DNS Queries */} - +
} + title={ + + } content={ Array.isArray(queries) ? ( <> - - - : - - - {client_resolver || '(unknown)'} - - - - {queries.map((query, index) => )} - +
+
+ + + : + +
+
{client_resolver || '(unknown)'}
+
+
+ {queries.map((query, index) => ( + + ))} +
) : ( - + ) } /> - +
{/* TCP COnnections */} - +
} + title={ + + } content={ tcpConnections.length > 0 ? ( tcpConnections.map((connection, index) => ( - - - - {connection.destination}: { - connection.failure ? - `${intl.formatMessage({id: 'Measurement.Details.Websites.TCP.ConnectionTo.Failed'})} (${connection.failure})` : - intl.formatMessage({id: 'Measurement.Details.Websites.TCP.ConnectionTo.Success'}) - } - - - +
+
+ {connection.destination}:{' '} + {connection.failure + ? `${intl.formatMessage({ id: 'Measurement.Details.Websites.TCP.ConnectionTo.Failed' })} (${connection.failure})` + : intl.formatMessage({ + id: 'Measurement.Details.Websites.TCP.ConnectionTo.Success', + })} +
+
)) ) : ( - + ) } /> - +
{/* I would like us to enrich the HTTP response body section with information about every request and response as this is a very common thing we look at when investigating a case. */} - +
} + title={ + + } content={ Array.isArray(requests) ? ( - - {requests.map((request, index) => )} - +
+ {requests.map((request, index) => ( + + ))} +
) : ( - + ) } /> - +
- ) + ), })} ) } - export default WebConnectivityDetails diff --git a/components/measurement/nettests/WhatsApp.js b/components/measurement/nettests/WhatsApp.js index f9ed14aa6..33b2d7acd 100644 --- a/components/measurement/nettests/WhatsApp.js +++ b/components/measurement/nettests/WhatsApp.js @@ -1,33 +1,19 @@ -import React from 'react' import PropTypes from 'prop-types' -import { - Heading, - Text, - Flex, - Box -} from 'ooni-components' +import { MdPersonAdd, MdPhoneAndroid, MdWebAsset } from 'react-icons/md' import { FormattedMessage, defineMessages } from 'react-intl' -import { MdPhoneAndroid } from 'react-icons/md' -import { MdWebAsset } from 'react-icons/md' -import { MdPersonAdd } from 'react-icons/md' -import styled from 'styled-components' - import AccessPointStatus from '../AccessPointStatus' import { DetailsBox } from '../DetailsBox' -const BugBox = styled(Box)` - margin-bottom: 30px; - background-color: ${props => props.theme.colors.yellow2}; - border: 1px solid ${props => props.theme.colors.yellow8}; - padding: 20px; -` - const WhatsAppDetails = ({ isAnomaly, scores, measurement, render }) => { const testKeys = measurement.test_keys const tcp_connect = testKeys.tcp_connect - let registrationServerAccessible, webAccessible, endpointsAccessible + let registrationServerAccessible + let webAccessible + let endpointsAccessible + try { - registrationServerAccessible = scores.analysis.registration_server_accessible + registrationServerAccessible = + scores.analysis.registration_server_accessible endpointsAccessible = scores.analysis.whatsapp_endpoints_accessible webAccessible = scores.analysis.whatsapp_web_accessible } catch (e) { @@ -39,27 +25,30 @@ const WhatsAppDetails = ({ isAnomaly, scores, measurement, render }) => { const messages = defineMessages({ reachable: { id: 'Measurement.Metadata.Whatsapp.Reachable', - defaultMessage: 'WhatsApp was reachable in {country}' + defaultMessage: 'WhatsApp was reachable in {country}', }, unReachable: { id: 'Measurement.Metadata.Whatsapp.UnReachable', - defaultMessage: 'WhatsApp was likely blocked in {country}' - } + defaultMessage: 'WhatsApp was likely blocked in {country}', + }, }) let status = 'reachable' - let info = + let info = ( + + ) let summaryText = 'Measurement.Details.SummaryText.WhatsApp.Reachable' if (isAnomaly) { status = 'anomaly' - info = + info = if (!endpointsAccessible) { summaryText = 'Measurement.Details.SummaryText.WhatsApp.AppFailure' } else if (!webAccessible) { summaryText = 'Measurement.Details.SummaryText.WhatsApp.DesktopFailure' } else if (!endpointsAccessible && !webAccessible) { - summaryText = 'Measurement.Details.SummaryText.WhatsApp.DesktopAndAppFailure' + summaryText = + 'Measurement.Details.SummaryText.WhatsApp.DesktopAndAppFailure' } } @@ -69,72 +58,94 @@ const WhatsAppDetails = ({ isAnomaly, scores, measurement, render }) => { summaryText: summaryText, headMetadata: { message: isAnomaly ? messages.unReachable : messages.reachable, - formatted: false + formatted: false, }, details: ( <> - - - +
+
+
} - label={} + label={ + + } ok={endpointsAccessible} /> - - +
+
} - label={} + label={ + + } ok={webAccessible} /> - - +
+
} - label={} + label={ + + } ok={registrationServerAccessible} /> - - - - {Array.isArray(tcp_connect) && tcp_connect.length > 0 && +
+
+
+ {Array.isArray(tcp_connect) && tcp_connect.length > 0 && ( <> } + title={ + + } content={ <> {tcp_connect.map((connection, index) => ( - - - - {connection.status.failure && - {connection.ip}:{connection.port} }} - /> - } - {connection.status.success && - {connection.ip}:{connection.port} }} - /> - } - - - +
+
+ {connection.status.failure && ( + + {' '} + {connection.ip}:{connection.port}{' '} + + ), + }} + /> + )} + {connection.status.success && ( + + {' '} + {connection.ip}:{connection.port}{' '} + + ), + }} + /> + )} +
+
))} } /> - } + )} - ) + ), }) } WhatsAppDetails.propTypes = { measurement: PropTypes.object.isRequired, - render: PropTypes.func + render: PropTypes.func, } export default WhatsAppDetails diff --git a/components/measurement/nettests/mlab_servers.json b/components/measurement/nettests/mlab_servers.json index 69be2c5f3..0cf90f4a9 100644 --- a/components/measurement/nettests/mlab_servers.json +++ b/components/measurement/nettests/mlab_servers.json @@ -1,1822 +1,1402 @@ [ - { - "city": "Auckland", - "country": "NZ", - "latitude": -36.85, - "longitude": 174.783, - "metro": [ - "akl01", - "akl" - ], - "roundrobin": true, - "site": "akl01", - "uplink_speed": "10g" - }, - { - "city": "Amsterdam", - "country": "NL", - "latitude": 52.3086, - "longitude": 4.7639, - "metro": [ - "ams03", - "ams" - ], - "roundrobin": true, - "site": "ams03", - "uplink_speed": "10g" - }, - { - "city": "Amsterdam", - "country": "NL", - "latitude": 52.3086, - "longitude": 4.7639, - "metro": [ - "ams04", - "ams" - ], - "roundrobin": true, - "site": "ams04", - "uplink_speed": "10g" - }, - { - "city": "Amsterdam", - "country": "NL", - "latitude": 52.3086, - "longitude": 4.7639, - "metro": [ - "ams05", - "ams" - ], - "roundrobin": true, - "site": "ams05", - "uplink_speed": "10g" - }, - { - "city": "Amsterdam", - "country": "NL", - "latitude": 52.3086, - "longitude": 4.7639, - "metro": [ - "ams07", - "ams" - ], - "roundrobin": true, - "site": "ams07", - "uplink_speed": "1g" - }, - { - "city": "Amsterdam", - "country": "NL", - "latitude": 52.3086, - "longitude": 4.7639, - "metro": [ - "ams08", - "ams" - ], - "roundrobin": true, - "site": "ams08", - "uplink_speed": "1g" - }, - { - "city": "Stockholm", - "country": "SE", - "latitude": 59.6519, - "longitude": 17.9186, - "metro": [ - "arn02", - "arn" - ], - "roundrobin": true, - "site": "arn02", - "uplink_speed": "10g" - }, - { - "city": "Stockholm", - "country": "SE", - "latitude": 59.6519, - "longitude": 17.9186, - "metro": [ - "arn03", - "arn" - ], - "roundrobin": true, - "site": "arn03", - "uplink_speed": "1g" - }, - { - "city": "Stockholm", - "country": "SE", - "latitude": 59.6519, - "longitude": 17.9186, - "metro": [ - "arn04", - "arn" - ], - "roundrobin": true, - "site": "arn04", - "uplink_speed": "10g" - }, - { - "city": "Stockholm", - "country": "SE", - "latitude": 59.6519, - "longitude": 17.9186, - "metro": [ - "arn05", - "arn" - ], - "roundrobin": true, - "site": "arn05", - "uplink_speed": "10g" - }, - { - "city": "Athens", - "country": "GR", - "latitude": 37.9364, - "longitude": 23.9444, - "metro": [ - "ath03", - "ath" - ], - "roundrobin": true, - "site": "ath03", - "uplink_speed": "10g" - }, - { - "city": "Atlanta", - "country": "US", - "latitude": 33.6367, - "longitude": -84.4281, - "metro": [ - "atl02", - "atl" - ], - "roundrobin": true, - "site": "atl02", - "uplink_speed": "10g" - }, - { - "city": "Atlanta", - "country": "US", - "latitude": 33.6367, - "longitude": -84.4281, - "metro": [ - "atl03", - "atl" - ], - "roundrobin": true, - "site": "atl03", - "uplink_speed": "1g" - }, - { - "city": "Atlanta", - "country": "US", - "latitude": 33.6367, - "longitude": -84.4281, - "metro": [ - "atl04", - "atl" - ], - "roundrobin": true, - "site": "atl04", - "uplink_speed": "10g" - }, - { - "city": "Atlanta", - "country": "US", - "latitude": 33.6367, - "longitude": -84.4281, - "metro": [ - "atl06", - "atl" - ], - "roundrobin": true, - "site": "atl06", - "uplink_speed": "1g" - }, - { - "city": "Atlanta", - "country": "US", - "latitude": 33.6367, - "longitude": -84.4281, - "metro": [ - "atl07", - "atl" - ], - "roundrobin": true, - "site": "atl07", - "uplink_speed": "10g" - }, - { - "city": "Atlanta", - "country": "US", - "latitude": 33.6367, - "longitude": -84.4281, - "metro": [ - "atl08", - "atl" - ], - "roundrobin": true, - "site": "atl08", - "uplink_speed": "1g" - }, - { - "city": "Barcelona", - "country": "ES", - "latitude": 41.2974, - "longitude": 2.0811, - "metro": [ - "bcn01", - "bcn" - ], - "roundrobin": true, - "site": "bcn01", - "uplink_speed": "10g" - }, - { - "city": "Belgrade", - "country": "RS", - "latitude": 44.8216, - "longitude": 20.2921, - "metro": [ - "beg01", - "beg" - ], - "roundrobin": true, - "site": "beg01", - "uplink_speed": "10g" - }, - { - "city": "Mumbai", - "country": "IN", - "latitude": 19.0886, - "longitude": 72.8681, - "metro": [ - "bom01", - "bom" - ], - "roundrobin": true, - "site": "bom01", - "uplink_speed": "1g" - }, - { - "city": "Mumbai", - "country": "IN", - "latitude": 19.0886, - "longitude": 72.8681, - "metro": [ - "bom02", - "bom" - ], - "roundrobin": true, - "site": "bom02", - "uplink_speed": "10g" - }, - { - "city": "Brussels", - "country": "BE", - "latitude": 50.4974, - "longitude": 3.3528, - "metro": [ - "bru01", - "bru" - ], - "roundrobin": true, - "site": "bru01", - "uplink_speed": "10g" - }, - { - "city": "Brussels", - "country": "BE", - "latitude": 50.4974, - "longitude": 3.3528, - "metro": [ - "bru02", - "bru" - ], - "roundrobin": true, - "site": "bru02", - "uplink_speed": "1g" - }, - { - "city": "Brussels", - "country": "BE", - "latitude": 50.4974, - "longitude": 3.3528, - "metro": [ - "bru03", - "bru" - ], - "roundrobin": true, - "site": "bru03", - "uplink_speed": "10g" - }, - { - "city": "Brussels", - "country": "BE", - "latitude": 50.4974, - "longitude": 3.3528, - "metro": [ - "bru04", - "bru" - ], - "roundrobin": true, - "site": "bru04", - "uplink_speed": "10g" - }, - { - "city": "Charleston", - "country": "US", - "latitude": 32.896663, - "longitude": -80.039184, - "metro": [ - "chs0c", - "chs" - ], - "roundrobin": true, - "site": "chs0c", - "uplink_speed": "1g" - }, - { - "city": "Cape", - "country": "ZA", - "latitude": -33.9724, - "longitude": 18.6018, - "metro": [ - "cpt01", - "cpt" - ], - "roundrobin": true, - "site": "cpt01", - "uplink_speed": "10g" - }, - { - "city": "New Delhi", - "country": "IN", - "latitude": 28.5562, - "longitude": 77.1, - "metro": [ - "del01", - "del" - ], - "roundrobin": true, - "site": "del01", - "uplink_speed": "10g" - }, - { - "city": "Denver", - "country": "US", - "latitude": 39.8561, - "longitude": -104.6737, - "metro": [ - "den02", - "den" - ], - "roundrobin": true, - "site": "den02", - "uplink_speed": "1g" - }, - { - "city": "Denver", - "country": "US", - "latitude": 39.8561, - "longitude": -104.6737, - "metro": [ - "den04", - "den" - ], - "roundrobin": true, - "site": "den04", - "uplink_speed": "10g" - }, - { - "city": "Denver", - "country": "US", - "latitude": 39.8561, - "longitude": -104.6737, - "metro": [ - "den05", - "den" - ], - "roundrobin": true, - "site": "den05", - "uplink_speed": "10g" - }, - { - "city": "Denver", - "country": "US", - "latitude": 39.8561, - "longitude": -104.6737, - "metro": [ - "den06", - "den" - ], - "roundrobin": true, - "site": "den06", - "uplink_speed": "10g" - }, - { - "city": "Dallas", - "country": "US", - "latitude": 32.8969, - "longitude": -97.0381, - "metro": [ - "dfw02", - "dfw" - ], - "roundrobin": true, - "site": "dfw02", - "uplink_speed": "1g" - }, - { - "city": "Dallas", - "country": "US", - "latitude": 32.8969, - "longitude": -97.0381, - "metro": [ - "dfw03", - "dfw" - ], - "roundrobin": true, - "site": "dfw03", - "uplink_speed": "1g" - }, - { - "city": "Dallas", - "country": "US", - "latitude": 32.8969, - "longitude": -97.0381, - "metro": [ - "dfw05", - "dfw" - ], - "roundrobin": true, - "site": "dfw05", - "uplink_speed": "10g" - }, - { - "city": "Dallas", - "country": "US", - "latitude": 32.8969, - "longitude": -97.0381, - "metro": [ - "dfw06", - "dfw" - ], - "roundrobin": true, - "site": "dfw06", - "uplink_speed": "1g" - }, - { - "city": "Dallas", - "country": "US", - "latitude": 32.8969, - "longitude": -97.0381, - "metro": [ - "dfw07", - "dfw" - ], - "roundrobin": true, - "site": "dfw07", - "uplink_speed": "10g" - }, - { - "city": "Dallas", - "country": "US", - "latitude": 32.8969, - "longitude": -97.0381, - "metro": [ - "dfw08", - "dfw" - ], - "roundrobin": true, - "site": "dfw08", - "uplink_speed": "10g" - }, - { - "city": "Dublin", - "country": "IE", - "latitude": 53.4333, - "longitude": -6.25, - "metro": [ - "dub01", - "dub" - ], - "roundrobin": true, - "site": "dub01", - "uplink_speed": "10g" - }, - { - "city": "Florianopolis", - "country": "BR", - "latitude": -27.6685, - "longitude": -48.546, - "metro": [ - "fln01", - "fln" - ], - "roundrobin": true, - "site": "fln01", - "uplink_speed": "10g" - }, - { - "city": "Frankfurt", - "country": "DE", - "latitude": 50.0379, - "longitude": 8.5622, - "metro": [ - "fra01", - "fra" - ], - "roundrobin": true, - "site": "fra01", - "uplink_speed": "10g" - }, - { - "city": "Frankfurt", - "country": "DE", - "latitude": 50.0379, - "longitude": 8.5622, - "metro": [ - "fra02", - "fra" - ], - "roundrobin": true, - "site": "fra02", - "uplink_speed": "10g" - }, - { - "city": "Frankfurt", - "country": "DE", - "latitude": 50.0379, - "longitude": 8.5622, - "metro": [ - "fra03", - "fra" - ], - "roundrobin": true, - "site": "fra03", - "uplink_speed": "10g" - }, - { - "city": "Frankfurt", - "country": "DE", - "latitude": 50.0379, - "longitude": 8.5622, - "metro": [ - "fra04", - "fra" - ], - "roundrobin": true, - "site": "fra04", - "uplink_speed": "1g" - }, - { - "city": "Hamburg", - "country": "DE", - "latitude": 53.6333, - "longitude": 9.9833, - "metro": [ - "ham02", - "ham" - ], - "roundrobin": true, - "site": "ham02", - "uplink_speed": "10g" - }, - { - "city": "Tokyo", - "country": "JP", - "latitude": 35.5522, - "longitude": 139.78, - "metro": [ - "hnd01", - "hnd" - ], - "roundrobin": true, - "site": "hnd01", - "uplink_speed": "1g" - }, - { - "city": "Tokyo", - "country": "JP", - "latitude": 35.5522, - "longitude": 139.78, - "metro": [ - "hnd02", - "hnd" - ], - "roundrobin": true, - "site": "hnd02", - "uplink_speed": "10g" - }, - { - "city": "Tokyo", - "country": "JP", - "latitude": 35.5522, - "longitude": 139.78, - "metro": [ - "hnd03", - "hnd" - ], - "roundrobin": true, - "site": "hnd03", - "uplink_speed": "10g" - }, - { - "city": "Washington", - "country": "US", - "latitude": 38.9444, - "longitude": -77.4558, - "metro": [ - "iad02", - "iad" - ], - "roundrobin": true, - "site": "iad02", - "uplink_speed": "10g" - }, - { - "city": "Washington", - "country": "US", - "latitude": 38.9444, - "longitude": -77.4558, - "metro": [ - "iad03", - "iad" - ], - "roundrobin": true, - "site": "iad03", - "uplink_speed": "1g" - }, - { - "city": "Washington", - "country": "US", - "latitude": 38.9444, - "longitude": -77.4558, - "metro": [ - "iad04", - "iad" - ], - "roundrobin": true, - "site": "iad04", - "uplink_speed": "10g" - }, - { - "city": "Washington", - "country": "US", - "latitude": 38.9444, - "longitude": -77.4558, - "metro": [ - "iad05", - "iad" - ], - "roundrobin": true, - "site": "iad05", - "uplink_speed": "1g" - }, - { - "city": "Washington", - "country": "US", - "latitude": 38.9444, - "longitude": -77.4558, - "metro": [ - "iad06", - "iad" - ], - "roundrobin": true, - "site": "iad06", - "uplink_speed": "10g" - }, - { - "city": "Washington", - "country": "US", - "latitude": 38.9444, - "longitude": -77.4558, - "metro": [ - "iad0c", - "iad" - ], - "roundrobin": true, - "site": "iad0c", - "uplink_speed": "1g" - }, - { - "city": "Johannesburg", - "country": "ZA", - "latitude": -26.2035, - "longitude": 28.1335, - "metro": [ - "jnb01", - "jnb" - ], - "roundrobin": true, - "site": "jnb01", - "uplink_speed": "10g" - }, - { - "city": "Los Angeles", - "country": "US", - "latitude": 33.9425, - "longitude": -118.4072, - "metro": [ - "lax02", - "lax" - ], - "roundrobin": true, - "site": "lax02", - "uplink_speed": "1g" - }, - { - "city": "Los Angeles", - "country": "US", - "latitude": 33.9425, - "longitude": -118.4072, - "metro": [ - "lax03", - "lax" - ], - "roundrobin": true, - "site": "lax03", - "uplink_speed": "10g" - }, - { - "city": "Los Angeles", - "country": "US", - "latitude": 33.9425, - "longitude": -118.4072, - "metro": [ - "lax04", - "lax" - ], - "roundrobin": true, - "site": "lax04", - "uplink_speed": "1g" - }, - { - "city": "Los Angeles", - "country": "US", - "latitude": 33.9425, - "longitude": -118.4072, - "metro": [ - "lax05", - "lax" - ], - "roundrobin": true, - "site": "lax05", - "uplink_speed": "10g" - }, - { - "city": "Los Angeles", - "country": "US", - "latitude": 33.9425, - "longitude": -118.4072, - "metro": [ - "lax06", - "lax" - ], - "roundrobin": true, - "site": "lax06", - "uplink_speed": "10g" - }, - { - "city": "Los Angeles", - "country": "US", - "latitude": 33.9425, - "longitude": -118.4072, - "metro": [ - "lax0c", - "lax" - ], - "roundrobin": true, - "site": "lax0c", - "uplink_speed": "1g" - }, - { - "city": "Larnaca", - "country": "CY", - "latitude": 34.8809, - "longitude": 33.626, - "metro": [ - "lca01", - "lca" - ], - "roundrobin": true, - "site": "lca01", - "uplink_speed": "1g" - }, - { - "city": "New York", - "country": "US", - "latitude": 40.7667, - "longitude": -73.8667, - "metro": [ - "lga03", - "lga" - ], - "roundrobin": true, - "site": "lga03", - "uplink_speed": "1g" - }, - { - "city": "New York", - "country": "US", - "latitude": 40.7667, - "longitude": -73.8667, - "metro": [ - "lga04", - "lga" - ], - "roundrobin": true, - "site": "lga04", - "uplink_speed": "10g" - }, - { - "city": "New York", - "country": "US", - "latitude": 40.7667, - "longitude": -73.8667, - "metro": [ - "lga05", - "lga" - ], - "roundrobin": true, - "site": "lga05", - "uplink_speed": "1g" - }, - { - "city": "New York", - "country": "US", - "latitude": 40.7667, - "longitude": -73.8667, - "metro": [ - "lga06", - "lga" - ], - "roundrobin": true, - "site": "lga06", - "uplink_speed": "10g" - }, - { - "city": "New York", - "country": "US", - "latitude": 40.7667, - "longitude": -73.8667, - "metro": [ - "lga07", - "lga" - ], - "roundrobin": true, - "site": "lga07", - "uplink_speed": "1g" - }, - { - "city": "New York", - "country": "US", - "latitude": 40.7667, - "longitude": -73.8667, - "metro": [ - "lga08", - "lga" - ], - "roundrobin": true, - "site": "lga08", - "uplink_speed": "10g" - }, - { - "city": "New York", - "country": "US", - "latitude": 40.7667, - "longitude": -73.8667, - "metro": [ - "lga0t", - "lga" - ], - "roundrobin": true, - "site": "lga0t", - "uplink_speed": "10g" - }, - { - "city": "New York", - "country": "US", - "latitude": 40.7667, - "longitude": -73.8667, - "metro": [ - "lga1t", - "lga" - ], - "roundrobin": true, - "site": "lga1t", - "uplink_speed": "1g" - }, - { - "city": "London", - "country": "GB", - "latitude": 51.4697, - "longitude": -0.4514, - "metro": [ - "lhr02", - "lhr" - ], - "roundrobin": true, - "site": "lhr02", - "uplink_speed": "10g" - }, - { - "city": "London", - "country": "GB", - "latitude": 51.4697, - "longitude": -0.4514, - "metro": [ - "lhr03", - "lhr" - ], - "roundrobin": true, - "site": "lhr03", - "uplink_speed": "10g" - }, - { - "city": "London", - "country": "GB", - "latitude": 51.4697, - "longitude": -0.4514, - "metro": [ - "lhr04", - "lhr" - ], - "roundrobin": true, - "site": "lhr04", - "uplink_speed": "10g" - }, - { - "city": "London", - "country": "GB", - "latitude": 51.4697, - "longitude": -0.4514, - "metro": [ - "lhr05", - "lhr" - ], - "roundrobin": true, - "site": "lhr05", - "uplink_speed": "1g" - }, - { - "city": "Lisbon", - "country": "PT", - "latitude": 38.7756, - "longitude": -9.1354, - "metro": [ - "lis01", - "lis" - ], - "roundrobin": true, - "site": "lis01", - "uplink_speed": "1g" - }, - { - "city": "Lisbon", - "country": "PT", - "latitude": 38.7756, - "longitude": -9.1354, - "metro": [ - "lis02", - "lis" - ], - "roundrobin": true, - "site": "lis02", - "uplink_speed": "10g" - }, - { - "city": "Ljubljana", - "country": "SI", - "latitude": 46.2236, - "longitude": 14.4575, - "metro": [ - "lju01", - "lju" - ], - "roundrobin": true, - "site": "lju01", - "uplink_speed": "1g" - }, - { - "city": "Chennai", - "country": "IN", - "latitude": 12.9941, - "longitude": 80.1709, - "metro": [ - "maa01", - "maa" - ], - "roundrobin": true, - "site": "maa01", - "uplink_speed": "10g" - }, - { - "city": "Madrid", - "country": "ES", - "latitude": 40.4667, - "longitude": -3.5667, - "metro": [ - "mad02", - "mad" - ], - "roundrobin": true, - "site": "mad02", - "uplink_speed": "1g" - }, - { - "city": "Madrid", - "country": "ES", - "latitude": 40.4667, - "longitude": -3.5667, - "metro": [ - "mad03", - "mad" - ], - "roundrobin": true, - "site": "mad03", - "uplink_speed": "10g" - }, - { - "city": "Madrid", - "country": "ES", - "latitude": 40.4667, - "longitude": -3.5667, - "metro": [ - "mad04", - "mad" - ], - "roundrobin": true, - "site": "mad04", - "uplink_speed": "10g" - }, - { - "city": "Miami", - "country": "US", - "latitude": 25.7833, - "longitude": -80.2667, - "metro": [ - "mia02", - "mia" - ], - "roundrobin": true, - "site": "mia02", - "uplink_speed": "10g" - }, - { - "city": "Miami", - "country": "US", - "latitude": 25.7833, - "longitude": -80.2667, - "metro": [ - "mia03", - "mia" - ], - "roundrobin": true, - "site": "mia03", - "uplink_speed": "1g" - }, - { - "city": "Miami", - "country": "US", - "latitude": 25.7833, - "longitude": -80.2667, - "metro": [ - "mia04", - "mia" - ], - "roundrobin": true, - "site": "mia04", - "uplink_speed": "10g" - }, - { - "city": "Miami", - "country": "US", - "latitude": 25.7833, - "longitude": -80.2667, - "metro": [ - "mia05", - "mia" - ], - "roundrobin": true, - "site": "mia05", - "uplink_speed": "10g" - }, - { - "city": "Miami", - "country": "US", - "latitude": 25.7833, - "longitude": -80.2667, - "metro": [ - "mia06", - "mia" - ], - "roundrobin": true, - "site": "mia06", - "uplink_speed": "1g" - }, - { - "city": "Milan", - "country": "IT", - "latitude": 45.464, - "longitude": 9.1916, - "metro": [ - "mil02", - "mil" - ], - "roundrobin": true, - "site": "mil02", - "uplink_speed": "10g" - }, - { - "city": "Milan", - "country": "IT", - "latitude": 45.464, - "longitude": 9.1916, - "metro": [ - "mil03", - "mil" - ], - "roundrobin": true, - "site": "mil03", - "uplink_speed": "10g" - }, - { - "city": "Milan", - "country": "IT", - "latitude": 45.464, - "longitude": 9.1916, - "metro": [ - "mil04", - "mil" - ], - "roundrobin": true, - "site": "mil04", - "uplink_speed": "1g" - }, - { - "city": "Milan", - "country": "IT", - "latitude": 45.464, - "longitude": 9.1916, - "metro": [ - "mil05", - "mil" - ], - "roundrobin": true, - "site": "mil05", - "uplink_speed": "10g" - }, - { - "city": "Manila", - "country": "PH", - "latitude": 14.5086, - "longitude": 121.0194, - "metro": [ - "mnl01", - "mnl" - ], - "roundrobin": true, - "site": "mnl01", - "uplink_speed": "10g" - }, - { - "city": "Maputo", - "country": "MZ", - "latitude": -25.9208, - "longitude": 32.5725, - "metro": [ - "mpm01", - "mpm" - ], - "roundrobin": true, - "site": "mpm01", - "uplink_speed": "1g" - }, - { - "city": "Nairobi", - "country": "KE", - "latitude": -1.3192, - "longitude": 36.9258, - "metro": [ - "nbo01", - "nbo" - ], - "roundrobin": true, - "site": "nbo01", - "uplink_speed": "10g" - }, - { - "city": "San Francisco Bay Area", - "country": "US", - "latitude": 37.3833, - "longitude": -122.0667, - "metro": [ - "nuq02", - "nuq" - ], - "roundrobin": true, - "site": "nuq02", - "uplink_speed": "10g" - }, - { - "city": "San Francisco Bay Area", - "country": "US", - "latitude": 37.3833, - "longitude": -122.0667, - "metro": [ - "nuq03", - "nuq" - ], - "roundrobin": true, - "site": "nuq03", - "uplink_speed": "10g" - }, - { - "city": "San Francisco Bay Area", - "country": "US", - "latitude": 37.3833, - "longitude": -122.0667, - "metro": [ - "nuq04", - "nuq" - ], - "roundrobin": true, - "site": "nuq04", - "uplink_speed": "1g" - }, - { - "city": "San Francisco Bay Area", - "country": "US", - "latitude": 37.3833, - "longitude": -122.0667, - "metro": [ - "nuq06", - "nuq" - ], - "roundrobin": true, - "site": "nuq06", - "uplink_speed": "10g" - }, - { - "city": "San Francisco Bay Area", - "country": "US", - "latitude": 37.3833, - "longitude": -122.0667, - "metro": [ - "nuq07", - "nuq" - ], - "roundrobin": true, - "site": "nuq07", - "uplink_speed": "10g" - }, - { - "city": "Omaha", - "country": "US", - "latitude": 41.30376, - "longitude": -95.893282, - "metro": [ - "oma0c", - "oma" - ], - "roundrobin": true, - "site": "oma0c", - "uplink_speed": "1g" - }, - { - "city": "Chicago", - "country": "US", - "latitude": 41.9786, - "longitude": -87.9047, - "metro": [ - "ord02", - "ord" - ], - "roundrobin": true, - "site": "ord02", - "uplink_speed": "10g" - }, - { - "city": "Chicago", - "country": "US", - "latitude": 41.9786, - "longitude": -87.9047, - "metro": [ - "ord03", - "ord" - ], - "roundrobin": true, - "site": "ord03", - "uplink_speed": "1g" - }, - { - "city": "Chicago", - "country": "US", - "latitude": 41.9786, - "longitude": -87.9047, - "metro": [ - "ord04", - "ord" - ], - "roundrobin": true, - "site": "ord04", - "uplink_speed": "10g" - }, - { - "city": "Chicago", - "country": "US", - "latitude": 41.9786, - "longitude": -87.9047, - "metro": [ - "ord05", - "ord" - ], - "roundrobin": true, - "site": "ord05", - "uplink_speed": "10g" - }, - { - "city": "Chicago", - "country": "US", - "latitude": 41.9786, - "longitude": -87.9047, - "metro": [ - "ord06", - "ord" - ], - "roundrobin": true, - "site": "ord06", - "uplink_speed": "1g" - }, - { - "city": "Paris", - "country": "FR", - "latitude": 48.8584, - "longitude": 2.349, - "metro": [ - "par02", - "par" - ], - "roundrobin": true, - "site": "par02", - "uplink_speed": "1g" - }, - { - "city": "Paris", - "country": "FR", - "latitude": 48.8584, - "longitude": 2.349, - "metro": [ - "par03", - "par" - ], - "roundrobin": true, - "site": "par03", - "uplink_speed": "10g" - }, - { - "city": "Paris", - "country": "FR", - "latitude": 48.8584, - "longitude": 2.349, - "metro": [ - "par04", - "par" - ], - "roundrobin": true, - "site": "par04", - "uplink_speed": "10g" - }, - { - "city": "Paris", - "country": "FR", - "latitude": 48.8584, - "longitude": 2.349, - "metro": [ - "par05", - "par" - ], - "roundrobin": true, - "site": "par05", - "uplink_speed": "10g" - }, - { - "city": "Portland", - "country": "US", - "latitude": 45.589191, - "longitude": -122.600228, - "metro": [ - "pdx0c", - "pdx" - ], - "roundrobin": true, - "site": "pdx0c", - "uplink_speed": "1g" - }, - { - "city": "Prague", - "country": "CZ", - "latitude": 50.0833, - "longitude": 14.4167, - "metro": [ - "prg02", - "prg" - ], - "roundrobin": true, - "site": "prg02", - "uplink_speed": "1g" - }, - { - "city": "Prague", - "country": "CZ", - "latitude": 50.0833, - "longitude": 14.4167, - "metro": [ - "prg03", - "prg" - ], - "roundrobin": true, - "site": "prg03", - "uplink_speed": "10g" - }, - { - "city": "Prague", - "country": "CZ", - "latitude": 50.0833, - "longitude": 14.4167, - "metro": [ - "prg04", - "prg" - ], - "roundrobin": true, - "site": "prg04", - "uplink_speed": "10g" - }, - { - "city": "Prague", - "country": "CZ", - "latitude": 50.0833, - "longitude": 14.4167, - "metro": [ - "prg05", - "prg" - ], - "roundrobin": true, - "site": "prg05", - "uplink_speed": "10g" - }, - { - "city": "Seattle", - "country": "US", - "latitude": 47.4489, - "longitude": -122.3094, - "metro": [ - "sea02", - "sea" - ], - "roundrobin": true, - "site": "sea02", - "uplink_speed": "10g" - }, - { - "city": "Seattle", - "country": "US", - "latitude": 47.4489, - "longitude": -122.3094, - "metro": [ - "sea03", - "sea" - ], - "roundrobin": true, - "site": "sea03", - "uplink_speed": "10g" - }, - { - "city": "Seattle", - "country": "US", - "latitude": 47.4489, - "longitude": -122.3094, - "metro": [ - "sea04", - "sea" - ], - "roundrobin": true, - "site": "sea04", - "uplink_speed": "1g" - }, - { - "city": "Seattle", - "country": "US", - "latitude": 47.4489, - "longitude": -122.3094, - "metro": [ - "sea06", - "sea" - ], - "roundrobin": true, - "site": "sea06", - "uplink_speed": "1g" - }, - { - "city": "Seattle", - "country": "US", - "latitude": 47.4489, - "longitude": -122.3094, - "metro": [ - "sea07", - "sea" - ], - "roundrobin": true, - "site": "sea07", - "uplink_speed": "10g" - }, - { - "city": "Seattle", - "country": "US", - "latitude": 47.4489, - "longitude": -122.3094, - "metro": [ - "sea08", - "sea" - ], - "roundrobin": true, - "site": "sea08", - "uplink_speed": "10g" - }, - { - "city": "Singapore", - "country": "SG", - "latitude": 1.355, - "longitude": 103.988, - "metro": [ - "sin01", - "sin" - ], - "roundrobin": true, - "site": "sin01", - "uplink_speed": "10g" - }, - { - "city": "San Francisco Bay Area", - "country": "US", - "latitude": 37.3833, - "longitude": -122.0667, - "metro": [ - "sjc01", - "sjc" - ], - "roundrobin": true, - "site": "sjc01", - "uplink_speed": "1g" - }, - { - "city": "Stavanger", - "country": "NO", - "latitude": 58.8767, - "longitude": 5.6378, - "metro": [ - "svg01", - "svg" - ], - "roundrobin": true, - "site": "svg01", - "uplink_speed": "10g" - }, - { - "city": "Sydney", - "country": "AU", - "latitude": -33.9461, - "longitude": 151.177, - "metro": [ - "syd02", - "syd" - ], - "roundrobin": true, - "site": "syd02", - "uplink_speed": "10g" - }, - { - "city": "Sydney", - "country": "AU", - "latitude": -33.9461, - "longitude": 151.177, - "metro": [ - "syd03", - "syd" - ], - "roundrobin": true, - "site": "syd03", - "uplink_speed": "10g" - }, - { - "city": "Podgorica", - "country": "ME", - "latitude": 42.1078, - "longitude": 18.7616, - "metro": [ - "tgd01", - "tgd" - ], - "roundrobin": true, - "site": "tgd01", - "uplink_speed": "10g" - }, - { - "city": "Antananarivo", - "country": "MG", - "latitude": -18.7969, - "longitude": 47.4788, - "metro": [ - "tnr01", - "tnr" - ], - "roundrobin": true, - "site": "tnr01", - "uplink_speed": "1g" - }, - { - "city": "Taipei", - "country": "TW", - "latitude": 25.0778, - "longitude": 121.224, - "metro": [ - "tpe01", - "tpe" - ], - "roundrobin": true, - "site": "tpe01", - "uplink_speed": "10g" - }, - { - "city": "Turin", - "country": "IT", - "latitude": 45.2008, - "longitude": 7.6497, - "metro": [ - "trn01", - "trn" - ], - "roundrobin": true, - "site": "trn01", - "uplink_speed": "10g" - }, - { - "city": "Tunis", - "country": "TN", - "latitude": 36.8516, - "longitude": 10.2291, - "metro": [ - "tun01", - "tun" - ], - "roundrobin": true, - "site": "tun01", - "uplink_speed": "1g" - }, - { - "city": "Tokyo", - "country": "US", - "latitude": 35.5522, - "longitude": 139.78, - "metro": [ - "tyo01", - "tyo" - ], - "roundrobin": true, - "site": "tyo01", - "uplink_speed": "1g" - }, - { - "city": "Tokyo", - "country": "US", - "latitude": 35.5522, - "longitude": 139.78, - "metro": [ - "tyo02", - "tyo" - ], - "roundrobin": true, - "site": "tyo02", - "uplink_speed": "1g" - }, - { - "city": "Tokyo", - "country": "US", - "latitude": 35.5522, - "longitude": 139.78, - "metro": [ - "tyo03", - "tyo" - ], - "roundrobin": true, - "site": "tyo03", - "uplink_speed": "1g" - }, - { - "city": "Vienna", - "country": "AT", - "latitude": 48.269, - "longitude": 16.4107, - "metro": [ - "vie01", - "vie" - ], - "roundrobin": true, - "site": "vie01", - "uplink_speed": "1g" - }, - { - "city": "Wellington", - "country": "NZ", - "latitude": -41.3272, - "longitude": 174.805, - "metro": [ - "wlg02", - "wlg" - ], - "roundrobin": true, - "site": "wlg02", - "uplink_speed": "10g" - }, - { - "city": "Moncton", - "country": "CA", - "latitude": 46.1073, - "longitude": -64.6738, - "metro": [ - "yqm01", - "yqm" - ], - "roundrobin": true, - "site": "yqm01", - "uplink_speed": "1g" - }, - { - "city": "Montreal", - "country": "CA", - "latitude": 45.4576, - "longitude": -73.7497, - "metro": [ - "yul02", - "yul" - ], - "roundrobin": true, - "site": "yul02", - "uplink_speed": "1g" - }, - { - "city": "Vancouver", - "country": "CA", - "latitude": 49.1902, - "longitude": -123.1837, - "metro": [ - "yvr01", - "yvr" - ], - "roundrobin": true, - "site": "yvr01", - "uplink_speed": "1g" - }, - { - "city": "Winnipeg", - "country": "CA", - "latitude": 49.906, - "longitude": -97.2373, - "metro": [ - "ywg01", - "ywg" - ], - "roundrobin": true, - "site": "ywg01", - "uplink_speed": "1g" - }, - { - "city": "Calgary", - "country": "CA", - "latitude": 51.1315, - "longitude": -114.0106, - "metro": [ - "yyc02", - "yyc" - ], - "roundrobin": true, - "site": "yyc02", - "uplink_speed": "1g" - }, - { - "city": "Toronto", - "country": "CA", - "latitude": 43.6767, - "longitude": -79.6306, - "metro": [ - "yyz02", - "yyz" - ], - "roundrobin": true, - "site": "yyz02", - "uplink_speed": "1g" - } + { + "city": "Auckland", + "country": "NZ", + "latitude": -36.85, + "longitude": 174.783, + "metro": ["akl01", "akl"], + "roundrobin": true, + "site": "akl01", + "uplink_speed": "10g" + }, + { + "city": "Amsterdam", + "country": "NL", + "latitude": 52.3086, + "longitude": 4.7639, + "metro": ["ams03", "ams"], + "roundrobin": true, + "site": "ams03", + "uplink_speed": "10g" + }, + { + "city": "Amsterdam", + "country": "NL", + "latitude": 52.3086, + "longitude": 4.7639, + "metro": ["ams04", "ams"], + "roundrobin": true, + "site": "ams04", + "uplink_speed": "10g" + }, + { + "city": "Amsterdam", + "country": "NL", + "latitude": 52.3086, + "longitude": 4.7639, + "metro": ["ams05", "ams"], + "roundrobin": true, + "site": "ams05", + "uplink_speed": "10g" + }, + { + "city": "Amsterdam", + "country": "NL", + "latitude": 52.3086, + "longitude": 4.7639, + "metro": ["ams07", "ams"], + "roundrobin": true, + "site": "ams07", + "uplink_speed": "1g" + }, + { + "city": "Amsterdam", + "country": "NL", + "latitude": 52.3086, + "longitude": 4.7639, + "metro": ["ams08", "ams"], + "roundrobin": true, + "site": "ams08", + "uplink_speed": "1g" + }, + { + "city": "Stockholm", + "country": "SE", + "latitude": 59.6519, + "longitude": 17.9186, + "metro": ["arn02", "arn"], + "roundrobin": true, + "site": "arn02", + "uplink_speed": "10g" + }, + { + "city": "Stockholm", + "country": "SE", + "latitude": 59.6519, + "longitude": 17.9186, + "metro": ["arn03", "arn"], + "roundrobin": true, + "site": "arn03", + "uplink_speed": "1g" + }, + { + "city": "Stockholm", + "country": "SE", + "latitude": 59.6519, + "longitude": 17.9186, + "metro": ["arn04", "arn"], + "roundrobin": true, + "site": "arn04", + "uplink_speed": "10g" + }, + { + "city": "Stockholm", + "country": "SE", + "latitude": 59.6519, + "longitude": 17.9186, + "metro": ["arn05", "arn"], + "roundrobin": true, + "site": "arn05", + "uplink_speed": "10g" + }, + { + "city": "Athens", + "country": "GR", + "latitude": 37.9364, + "longitude": 23.9444, + "metro": ["ath03", "ath"], + "roundrobin": true, + "site": "ath03", + "uplink_speed": "10g" + }, + { + "city": "Atlanta", + "country": "US", + "latitude": 33.6367, + "longitude": -84.4281, + "metro": ["atl02", "atl"], + "roundrobin": true, + "site": "atl02", + "uplink_speed": "10g" + }, + { + "city": "Atlanta", + "country": "US", + "latitude": 33.6367, + "longitude": -84.4281, + "metro": ["atl03", "atl"], + "roundrobin": true, + "site": "atl03", + "uplink_speed": "1g" + }, + { + "city": "Atlanta", + "country": "US", + "latitude": 33.6367, + "longitude": -84.4281, + "metro": ["atl04", "atl"], + "roundrobin": true, + "site": "atl04", + "uplink_speed": "10g" + }, + { + "city": "Atlanta", + "country": "US", + "latitude": 33.6367, + "longitude": -84.4281, + "metro": ["atl06", "atl"], + "roundrobin": true, + "site": "atl06", + "uplink_speed": "1g" + }, + { + "city": "Atlanta", + "country": "US", + "latitude": 33.6367, + "longitude": -84.4281, + "metro": ["atl07", "atl"], + "roundrobin": true, + "site": "atl07", + "uplink_speed": "10g" + }, + { + "city": "Atlanta", + "country": "US", + "latitude": 33.6367, + "longitude": -84.4281, + "metro": ["atl08", "atl"], + "roundrobin": true, + "site": "atl08", + "uplink_speed": "1g" + }, + { + "city": "Barcelona", + "country": "ES", + "latitude": 41.2974, + "longitude": 2.0811, + "metro": ["bcn01", "bcn"], + "roundrobin": true, + "site": "bcn01", + "uplink_speed": "10g" + }, + { + "city": "Belgrade", + "country": "RS", + "latitude": 44.8216, + "longitude": 20.2921, + "metro": ["beg01", "beg"], + "roundrobin": true, + "site": "beg01", + "uplink_speed": "10g" + }, + { + "city": "Mumbai", + "country": "IN", + "latitude": 19.0886, + "longitude": 72.8681, + "metro": ["bom01", "bom"], + "roundrobin": true, + "site": "bom01", + "uplink_speed": "1g" + }, + { + "city": "Mumbai", + "country": "IN", + "latitude": 19.0886, + "longitude": 72.8681, + "metro": ["bom02", "bom"], + "roundrobin": true, + "site": "bom02", + "uplink_speed": "10g" + }, + { + "city": "Brussels", + "country": "BE", + "latitude": 50.4974, + "longitude": 3.3528, + "metro": ["bru01", "bru"], + "roundrobin": true, + "site": "bru01", + "uplink_speed": "10g" + }, + { + "city": "Brussels", + "country": "BE", + "latitude": 50.4974, + "longitude": 3.3528, + "metro": ["bru02", "bru"], + "roundrobin": true, + "site": "bru02", + "uplink_speed": "1g" + }, + { + "city": "Brussels", + "country": "BE", + "latitude": 50.4974, + "longitude": 3.3528, + "metro": ["bru03", "bru"], + "roundrobin": true, + "site": "bru03", + "uplink_speed": "10g" + }, + { + "city": "Brussels", + "country": "BE", + "latitude": 50.4974, + "longitude": 3.3528, + "metro": ["bru04", "bru"], + "roundrobin": true, + "site": "bru04", + "uplink_speed": "10g" + }, + { + "city": "Charleston", + "country": "US", + "latitude": 32.896663, + "longitude": -80.039184, + "metro": ["chs0c", "chs"], + "roundrobin": true, + "site": "chs0c", + "uplink_speed": "1g" + }, + { + "city": "Cape", + "country": "ZA", + "latitude": -33.9724, + "longitude": 18.6018, + "metro": ["cpt01", "cpt"], + "roundrobin": true, + "site": "cpt01", + "uplink_speed": "10g" + }, + { + "city": "New Delhi", + "country": "IN", + "latitude": 28.5562, + "longitude": 77.1, + "metro": ["del01", "del"], + "roundrobin": true, + "site": "del01", + "uplink_speed": "10g" + }, + { + "city": "Denver", + "country": "US", + "latitude": 39.8561, + "longitude": -104.6737, + "metro": ["den02", "den"], + "roundrobin": true, + "site": "den02", + "uplink_speed": "1g" + }, + { + "city": "Denver", + "country": "US", + "latitude": 39.8561, + "longitude": -104.6737, + "metro": ["den04", "den"], + "roundrobin": true, + "site": "den04", + "uplink_speed": "10g" + }, + { + "city": "Denver", + "country": "US", + "latitude": 39.8561, + "longitude": -104.6737, + "metro": ["den05", "den"], + "roundrobin": true, + "site": "den05", + "uplink_speed": "10g" + }, + { + "city": "Denver", + "country": "US", + "latitude": 39.8561, + "longitude": -104.6737, + "metro": ["den06", "den"], + "roundrobin": true, + "site": "den06", + "uplink_speed": "10g" + }, + { + "city": "Dallas", + "country": "US", + "latitude": 32.8969, + "longitude": -97.0381, + "metro": ["dfw02", "dfw"], + "roundrobin": true, + "site": "dfw02", + "uplink_speed": "1g" + }, + { + "city": "Dallas", + "country": "US", + "latitude": 32.8969, + "longitude": -97.0381, + "metro": ["dfw03", "dfw"], + "roundrobin": true, + "site": "dfw03", + "uplink_speed": "1g" + }, + { + "city": "Dallas", + "country": "US", + "latitude": 32.8969, + "longitude": -97.0381, + "metro": ["dfw05", "dfw"], + "roundrobin": true, + "site": "dfw05", + "uplink_speed": "10g" + }, + { + "city": "Dallas", + "country": "US", + "latitude": 32.8969, + "longitude": -97.0381, + "metro": ["dfw06", "dfw"], + "roundrobin": true, + "site": "dfw06", + "uplink_speed": "1g" + }, + { + "city": "Dallas", + "country": "US", + "latitude": 32.8969, + "longitude": -97.0381, + "metro": ["dfw07", "dfw"], + "roundrobin": true, + "site": "dfw07", + "uplink_speed": "10g" + }, + { + "city": "Dallas", + "country": "US", + "latitude": 32.8969, + "longitude": -97.0381, + "metro": ["dfw08", "dfw"], + "roundrobin": true, + "site": "dfw08", + "uplink_speed": "10g" + }, + { + "city": "Dublin", + "country": "IE", + "latitude": 53.4333, + "longitude": -6.25, + "metro": ["dub01", "dub"], + "roundrobin": true, + "site": "dub01", + "uplink_speed": "10g" + }, + { + "city": "Florianopolis", + "country": "BR", + "latitude": -27.6685, + "longitude": -48.546, + "metro": ["fln01", "fln"], + "roundrobin": true, + "site": "fln01", + "uplink_speed": "10g" + }, + { + "city": "Frankfurt", + "country": "DE", + "latitude": 50.0379, + "longitude": 8.5622, + "metro": ["fra01", "fra"], + "roundrobin": true, + "site": "fra01", + "uplink_speed": "10g" + }, + { + "city": "Frankfurt", + "country": "DE", + "latitude": 50.0379, + "longitude": 8.5622, + "metro": ["fra02", "fra"], + "roundrobin": true, + "site": "fra02", + "uplink_speed": "10g" + }, + { + "city": "Frankfurt", + "country": "DE", + "latitude": 50.0379, + "longitude": 8.5622, + "metro": ["fra03", "fra"], + "roundrobin": true, + "site": "fra03", + "uplink_speed": "10g" + }, + { + "city": "Frankfurt", + "country": "DE", + "latitude": 50.0379, + "longitude": 8.5622, + "metro": ["fra04", "fra"], + "roundrobin": true, + "site": "fra04", + "uplink_speed": "1g" + }, + { + "city": "Hamburg", + "country": "DE", + "latitude": 53.6333, + "longitude": 9.9833, + "metro": ["ham02", "ham"], + "roundrobin": true, + "site": "ham02", + "uplink_speed": "10g" + }, + { + "city": "Tokyo", + "country": "JP", + "latitude": 35.5522, + "longitude": 139.78, + "metro": ["hnd01", "hnd"], + "roundrobin": true, + "site": "hnd01", + "uplink_speed": "1g" + }, + { + "city": "Tokyo", + "country": "JP", + "latitude": 35.5522, + "longitude": 139.78, + "metro": ["hnd02", "hnd"], + "roundrobin": true, + "site": "hnd02", + "uplink_speed": "10g" + }, + { + "city": "Tokyo", + "country": "JP", + "latitude": 35.5522, + "longitude": 139.78, + "metro": ["hnd03", "hnd"], + "roundrobin": true, + "site": "hnd03", + "uplink_speed": "10g" + }, + { + "city": "Washington", + "country": "US", + "latitude": 38.9444, + "longitude": -77.4558, + "metro": ["iad02", "iad"], + "roundrobin": true, + "site": "iad02", + "uplink_speed": "10g" + }, + { + "city": "Washington", + "country": "US", + "latitude": 38.9444, + "longitude": -77.4558, + "metro": ["iad03", "iad"], + "roundrobin": true, + "site": "iad03", + "uplink_speed": "1g" + }, + { + "city": "Washington", + "country": "US", + "latitude": 38.9444, + "longitude": -77.4558, + "metro": ["iad04", "iad"], + "roundrobin": true, + "site": "iad04", + "uplink_speed": "10g" + }, + { + "city": "Washington", + "country": "US", + "latitude": 38.9444, + "longitude": -77.4558, + "metro": ["iad05", "iad"], + "roundrobin": true, + "site": "iad05", + "uplink_speed": "1g" + }, + { + "city": "Washington", + "country": "US", + "latitude": 38.9444, + "longitude": -77.4558, + "metro": ["iad06", "iad"], + "roundrobin": true, + "site": "iad06", + "uplink_speed": "10g" + }, + { + "city": "Washington", + "country": "US", + "latitude": 38.9444, + "longitude": -77.4558, + "metro": ["iad0c", "iad"], + "roundrobin": true, + "site": "iad0c", + "uplink_speed": "1g" + }, + { + "city": "Johannesburg", + "country": "ZA", + "latitude": -26.2035, + "longitude": 28.1335, + "metro": ["jnb01", "jnb"], + "roundrobin": true, + "site": "jnb01", + "uplink_speed": "10g" + }, + { + "city": "Los Angeles", + "country": "US", + "latitude": 33.9425, + "longitude": -118.4072, + "metro": ["lax02", "lax"], + "roundrobin": true, + "site": "lax02", + "uplink_speed": "1g" + }, + { + "city": "Los Angeles", + "country": "US", + "latitude": 33.9425, + "longitude": -118.4072, + "metro": ["lax03", "lax"], + "roundrobin": true, + "site": "lax03", + "uplink_speed": "10g" + }, + { + "city": "Los Angeles", + "country": "US", + "latitude": 33.9425, + "longitude": -118.4072, + "metro": ["lax04", "lax"], + "roundrobin": true, + "site": "lax04", + "uplink_speed": "1g" + }, + { + "city": "Los Angeles", + "country": "US", + "latitude": 33.9425, + "longitude": -118.4072, + "metro": ["lax05", "lax"], + "roundrobin": true, + "site": "lax05", + "uplink_speed": "10g" + }, + { + "city": "Los Angeles", + "country": "US", + "latitude": 33.9425, + "longitude": -118.4072, + "metro": ["lax06", "lax"], + "roundrobin": true, + "site": "lax06", + "uplink_speed": "10g" + }, + { + "city": "Los Angeles", + "country": "US", + "latitude": 33.9425, + "longitude": -118.4072, + "metro": ["lax0c", "lax"], + "roundrobin": true, + "site": "lax0c", + "uplink_speed": "1g" + }, + { + "city": "Larnaca", + "country": "CY", + "latitude": 34.8809, + "longitude": 33.626, + "metro": ["lca01", "lca"], + "roundrobin": true, + "site": "lca01", + "uplink_speed": "1g" + }, + { + "city": "New York", + "country": "US", + "latitude": 40.7667, + "longitude": -73.8667, + "metro": ["lga03", "lga"], + "roundrobin": true, + "site": "lga03", + "uplink_speed": "1g" + }, + { + "city": "New York", + "country": "US", + "latitude": 40.7667, + "longitude": -73.8667, + "metro": ["lga04", "lga"], + "roundrobin": true, + "site": "lga04", + "uplink_speed": "10g" + }, + { + "city": "New York", + "country": "US", + "latitude": 40.7667, + "longitude": -73.8667, + "metro": ["lga05", "lga"], + "roundrobin": true, + "site": "lga05", + "uplink_speed": "1g" + }, + { + "city": "New York", + "country": "US", + "latitude": 40.7667, + "longitude": -73.8667, + "metro": ["lga06", "lga"], + "roundrobin": true, + "site": "lga06", + "uplink_speed": "10g" + }, + { + "city": "New York", + "country": "US", + "latitude": 40.7667, + "longitude": -73.8667, + "metro": ["lga07", "lga"], + "roundrobin": true, + "site": "lga07", + "uplink_speed": "1g" + }, + { + "city": "New York", + "country": "US", + "latitude": 40.7667, + "longitude": -73.8667, + "metro": ["lga08", "lga"], + "roundrobin": true, + "site": "lga08", + "uplink_speed": "10g" + }, + { + "city": "New York", + "country": "US", + "latitude": 40.7667, + "longitude": -73.8667, + "metro": ["lga0t", "lga"], + "roundrobin": true, + "site": "lga0t", + "uplink_speed": "10g" + }, + { + "city": "New York", + "country": "US", + "latitude": 40.7667, + "longitude": -73.8667, + "metro": ["lga1t", "lga"], + "roundrobin": true, + "site": "lga1t", + "uplink_speed": "1g" + }, + { + "city": "London", + "country": "GB", + "latitude": 51.4697, + "longitude": -0.4514, + "metro": ["lhr02", "lhr"], + "roundrobin": true, + "site": "lhr02", + "uplink_speed": "10g" + }, + { + "city": "London", + "country": "GB", + "latitude": 51.4697, + "longitude": -0.4514, + "metro": ["lhr03", "lhr"], + "roundrobin": true, + "site": "lhr03", + "uplink_speed": "10g" + }, + { + "city": "London", + "country": "GB", + "latitude": 51.4697, + "longitude": -0.4514, + "metro": ["lhr04", "lhr"], + "roundrobin": true, + "site": "lhr04", + "uplink_speed": "10g" + }, + { + "city": "London", + "country": "GB", + "latitude": 51.4697, + "longitude": -0.4514, + "metro": ["lhr05", "lhr"], + "roundrobin": true, + "site": "lhr05", + "uplink_speed": "1g" + }, + { + "city": "Lisbon", + "country": "PT", + "latitude": 38.7756, + "longitude": -9.1354, + "metro": ["lis01", "lis"], + "roundrobin": true, + "site": "lis01", + "uplink_speed": "1g" + }, + { + "city": "Lisbon", + "country": "PT", + "latitude": 38.7756, + "longitude": -9.1354, + "metro": ["lis02", "lis"], + "roundrobin": true, + "site": "lis02", + "uplink_speed": "10g" + }, + { + "city": "Ljubljana", + "country": "SI", + "latitude": 46.2236, + "longitude": 14.4575, + "metro": ["lju01", "lju"], + "roundrobin": true, + "site": "lju01", + "uplink_speed": "1g" + }, + { + "city": "Chennai", + "country": "IN", + "latitude": 12.9941, + "longitude": 80.1709, + "metro": ["maa01", "maa"], + "roundrobin": true, + "site": "maa01", + "uplink_speed": "10g" + }, + { + "city": "Madrid", + "country": "ES", + "latitude": 40.4667, + "longitude": -3.5667, + "metro": ["mad02", "mad"], + "roundrobin": true, + "site": "mad02", + "uplink_speed": "1g" + }, + { + "city": "Madrid", + "country": "ES", + "latitude": 40.4667, + "longitude": -3.5667, + "metro": ["mad03", "mad"], + "roundrobin": true, + "site": "mad03", + "uplink_speed": "10g" + }, + { + "city": "Madrid", + "country": "ES", + "latitude": 40.4667, + "longitude": -3.5667, + "metro": ["mad04", "mad"], + "roundrobin": true, + "site": "mad04", + "uplink_speed": "10g" + }, + { + "city": "Miami", + "country": "US", + "latitude": 25.7833, + "longitude": -80.2667, + "metro": ["mia02", "mia"], + "roundrobin": true, + "site": "mia02", + "uplink_speed": "10g" + }, + { + "city": "Miami", + "country": "US", + "latitude": 25.7833, + "longitude": -80.2667, + "metro": ["mia03", "mia"], + "roundrobin": true, + "site": "mia03", + "uplink_speed": "1g" + }, + { + "city": "Miami", + "country": "US", + "latitude": 25.7833, + "longitude": -80.2667, + "metro": ["mia04", "mia"], + "roundrobin": true, + "site": "mia04", + "uplink_speed": "10g" + }, + { + "city": "Miami", + "country": "US", + "latitude": 25.7833, + "longitude": -80.2667, + "metro": ["mia05", "mia"], + "roundrobin": true, + "site": "mia05", + "uplink_speed": "10g" + }, + { + "city": "Miami", + "country": "US", + "latitude": 25.7833, + "longitude": -80.2667, + "metro": ["mia06", "mia"], + "roundrobin": true, + "site": "mia06", + "uplink_speed": "1g" + }, + { + "city": "Milan", + "country": "IT", + "latitude": 45.464, + "longitude": 9.1916, + "metro": ["mil02", "mil"], + "roundrobin": true, + "site": "mil02", + "uplink_speed": "10g" + }, + { + "city": "Milan", + "country": "IT", + "latitude": 45.464, + "longitude": 9.1916, + "metro": ["mil03", "mil"], + "roundrobin": true, + "site": "mil03", + "uplink_speed": "10g" + }, + { + "city": "Milan", + "country": "IT", + "latitude": 45.464, + "longitude": 9.1916, + "metro": ["mil04", "mil"], + "roundrobin": true, + "site": "mil04", + "uplink_speed": "1g" + }, + { + "city": "Milan", + "country": "IT", + "latitude": 45.464, + "longitude": 9.1916, + "metro": ["mil05", "mil"], + "roundrobin": true, + "site": "mil05", + "uplink_speed": "10g" + }, + { + "city": "Manila", + "country": "PH", + "latitude": 14.5086, + "longitude": 121.0194, + "metro": ["mnl01", "mnl"], + "roundrobin": true, + "site": "mnl01", + "uplink_speed": "10g" + }, + { + "city": "Maputo", + "country": "MZ", + "latitude": -25.9208, + "longitude": 32.5725, + "metro": ["mpm01", "mpm"], + "roundrobin": true, + "site": "mpm01", + "uplink_speed": "1g" + }, + { + "city": "Nairobi", + "country": "KE", + "latitude": -1.3192, + "longitude": 36.9258, + "metro": ["nbo01", "nbo"], + "roundrobin": true, + "site": "nbo01", + "uplink_speed": "10g" + }, + { + "city": "San Francisco Bay Area", + "country": "US", + "latitude": 37.3833, + "longitude": -122.0667, + "metro": ["nuq02", "nuq"], + "roundrobin": true, + "site": "nuq02", + "uplink_speed": "10g" + }, + { + "city": "San Francisco Bay Area", + "country": "US", + "latitude": 37.3833, + "longitude": -122.0667, + "metro": ["nuq03", "nuq"], + "roundrobin": true, + "site": "nuq03", + "uplink_speed": "10g" + }, + { + "city": "San Francisco Bay Area", + "country": "US", + "latitude": 37.3833, + "longitude": -122.0667, + "metro": ["nuq04", "nuq"], + "roundrobin": true, + "site": "nuq04", + "uplink_speed": "1g" + }, + { + "city": "San Francisco Bay Area", + "country": "US", + "latitude": 37.3833, + "longitude": -122.0667, + "metro": ["nuq06", "nuq"], + "roundrobin": true, + "site": "nuq06", + "uplink_speed": "10g" + }, + { + "city": "San Francisco Bay Area", + "country": "US", + "latitude": 37.3833, + "longitude": -122.0667, + "metro": ["nuq07", "nuq"], + "roundrobin": true, + "site": "nuq07", + "uplink_speed": "10g" + }, + { + "city": "Omaha", + "country": "US", + "latitude": 41.30376, + "longitude": -95.893282, + "metro": ["oma0c", "oma"], + "roundrobin": true, + "site": "oma0c", + "uplink_speed": "1g" + }, + { + "city": "Chicago", + "country": "US", + "latitude": 41.9786, + "longitude": -87.9047, + "metro": ["ord02", "ord"], + "roundrobin": true, + "site": "ord02", + "uplink_speed": "10g" + }, + { + "city": "Chicago", + "country": "US", + "latitude": 41.9786, + "longitude": -87.9047, + "metro": ["ord03", "ord"], + "roundrobin": true, + "site": "ord03", + "uplink_speed": "1g" + }, + { + "city": "Chicago", + "country": "US", + "latitude": 41.9786, + "longitude": -87.9047, + "metro": ["ord04", "ord"], + "roundrobin": true, + "site": "ord04", + "uplink_speed": "10g" + }, + { + "city": "Chicago", + "country": "US", + "latitude": 41.9786, + "longitude": -87.9047, + "metro": ["ord05", "ord"], + "roundrobin": true, + "site": "ord05", + "uplink_speed": "10g" + }, + { + "city": "Chicago", + "country": "US", + "latitude": 41.9786, + "longitude": -87.9047, + "metro": ["ord06", "ord"], + "roundrobin": true, + "site": "ord06", + "uplink_speed": "1g" + }, + { + "city": "Paris", + "country": "FR", + "latitude": 48.8584, + "longitude": 2.349, + "metro": ["par02", "par"], + "roundrobin": true, + "site": "par02", + "uplink_speed": "1g" + }, + { + "city": "Paris", + "country": "FR", + "latitude": 48.8584, + "longitude": 2.349, + "metro": ["par03", "par"], + "roundrobin": true, + "site": "par03", + "uplink_speed": "10g" + }, + { + "city": "Paris", + "country": "FR", + "latitude": 48.8584, + "longitude": 2.349, + "metro": ["par04", "par"], + "roundrobin": true, + "site": "par04", + "uplink_speed": "10g" + }, + { + "city": "Paris", + "country": "FR", + "latitude": 48.8584, + "longitude": 2.349, + "metro": ["par05", "par"], + "roundrobin": true, + "site": "par05", + "uplink_speed": "10g" + }, + { + "city": "Portland", + "country": "US", + "latitude": 45.589191, + "longitude": -122.600228, + "metro": ["pdx0c", "pdx"], + "roundrobin": true, + "site": "pdx0c", + "uplink_speed": "1g" + }, + { + "city": "Prague", + "country": "CZ", + "latitude": 50.0833, + "longitude": 14.4167, + "metro": ["prg02", "prg"], + "roundrobin": true, + "site": "prg02", + "uplink_speed": "1g" + }, + { + "city": "Prague", + "country": "CZ", + "latitude": 50.0833, + "longitude": 14.4167, + "metro": ["prg03", "prg"], + "roundrobin": true, + "site": "prg03", + "uplink_speed": "10g" + }, + { + "city": "Prague", + "country": "CZ", + "latitude": 50.0833, + "longitude": 14.4167, + "metro": ["prg04", "prg"], + "roundrobin": true, + "site": "prg04", + "uplink_speed": "10g" + }, + { + "city": "Prague", + "country": "CZ", + "latitude": 50.0833, + "longitude": 14.4167, + "metro": ["prg05", "prg"], + "roundrobin": true, + "site": "prg05", + "uplink_speed": "10g" + }, + { + "city": "Seattle", + "country": "US", + "latitude": 47.4489, + "longitude": -122.3094, + "metro": ["sea02", "sea"], + "roundrobin": true, + "site": "sea02", + "uplink_speed": "10g" + }, + { + "city": "Seattle", + "country": "US", + "latitude": 47.4489, + "longitude": -122.3094, + "metro": ["sea03", "sea"], + "roundrobin": true, + "site": "sea03", + "uplink_speed": "10g" + }, + { + "city": "Seattle", + "country": "US", + "latitude": 47.4489, + "longitude": -122.3094, + "metro": ["sea04", "sea"], + "roundrobin": true, + "site": "sea04", + "uplink_speed": "1g" + }, + { + "city": "Seattle", + "country": "US", + "latitude": 47.4489, + "longitude": -122.3094, + "metro": ["sea06", "sea"], + "roundrobin": true, + "site": "sea06", + "uplink_speed": "1g" + }, + { + "city": "Seattle", + "country": "US", + "latitude": 47.4489, + "longitude": -122.3094, + "metro": ["sea07", "sea"], + "roundrobin": true, + "site": "sea07", + "uplink_speed": "10g" + }, + { + "city": "Seattle", + "country": "US", + "latitude": 47.4489, + "longitude": -122.3094, + "metro": ["sea08", "sea"], + "roundrobin": true, + "site": "sea08", + "uplink_speed": "10g" + }, + { + "city": "Singapore", + "country": "SG", + "latitude": 1.355, + "longitude": 103.988, + "metro": ["sin01", "sin"], + "roundrobin": true, + "site": "sin01", + "uplink_speed": "10g" + }, + { + "city": "San Francisco Bay Area", + "country": "US", + "latitude": 37.3833, + "longitude": -122.0667, + "metro": ["sjc01", "sjc"], + "roundrobin": true, + "site": "sjc01", + "uplink_speed": "1g" + }, + { + "city": "Stavanger", + "country": "NO", + "latitude": 58.8767, + "longitude": 5.6378, + "metro": ["svg01", "svg"], + "roundrobin": true, + "site": "svg01", + "uplink_speed": "10g" + }, + { + "city": "Sydney", + "country": "AU", + "latitude": -33.9461, + "longitude": 151.177, + "metro": ["syd02", "syd"], + "roundrobin": true, + "site": "syd02", + "uplink_speed": "10g" + }, + { + "city": "Sydney", + "country": "AU", + "latitude": -33.9461, + "longitude": 151.177, + "metro": ["syd03", "syd"], + "roundrobin": true, + "site": "syd03", + "uplink_speed": "10g" + }, + { + "city": "Podgorica", + "country": "ME", + "latitude": 42.1078, + "longitude": 18.7616, + "metro": ["tgd01", "tgd"], + "roundrobin": true, + "site": "tgd01", + "uplink_speed": "10g" + }, + { + "city": "Antananarivo", + "country": "MG", + "latitude": -18.7969, + "longitude": 47.4788, + "metro": ["tnr01", "tnr"], + "roundrobin": true, + "site": "tnr01", + "uplink_speed": "1g" + }, + { + "city": "Taipei", + "country": "TW", + "latitude": 25.0778, + "longitude": 121.224, + "metro": ["tpe01", "tpe"], + "roundrobin": true, + "site": "tpe01", + "uplink_speed": "10g" + }, + { + "city": "Turin", + "country": "IT", + "latitude": 45.2008, + "longitude": 7.6497, + "metro": ["trn01", "trn"], + "roundrobin": true, + "site": "trn01", + "uplink_speed": "10g" + }, + { + "city": "Tunis", + "country": "TN", + "latitude": 36.8516, + "longitude": 10.2291, + "metro": ["tun01", "tun"], + "roundrobin": true, + "site": "tun01", + "uplink_speed": "1g" + }, + { + "city": "Tokyo", + "country": "US", + "latitude": 35.5522, + "longitude": 139.78, + "metro": ["tyo01", "tyo"], + "roundrobin": true, + "site": "tyo01", + "uplink_speed": "1g" + }, + { + "city": "Tokyo", + "country": "US", + "latitude": 35.5522, + "longitude": 139.78, + "metro": ["tyo02", "tyo"], + "roundrobin": true, + "site": "tyo02", + "uplink_speed": "1g" + }, + { + "city": "Tokyo", + "country": "US", + "latitude": 35.5522, + "longitude": 139.78, + "metro": ["tyo03", "tyo"], + "roundrobin": true, + "site": "tyo03", + "uplink_speed": "1g" + }, + { + "city": "Vienna", + "country": "AT", + "latitude": 48.269, + "longitude": 16.4107, + "metro": ["vie01", "vie"], + "roundrobin": true, + "site": "vie01", + "uplink_speed": "1g" + }, + { + "city": "Wellington", + "country": "NZ", + "latitude": -41.3272, + "longitude": 174.805, + "metro": ["wlg02", "wlg"], + "roundrobin": true, + "site": "wlg02", + "uplink_speed": "10g" + }, + { + "city": "Moncton", + "country": "CA", + "latitude": 46.1073, + "longitude": -64.6738, + "metro": ["yqm01", "yqm"], + "roundrobin": true, + "site": "yqm01", + "uplink_speed": "1g" + }, + { + "city": "Montreal", + "country": "CA", + "latitude": 45.4576, + "longitude": -73.7497, + "metro": ["yul02", "yul"], + "roundrobin": true, + "site": "yul02", + "uplink_speed": "1g" + }, + { + "city": "Vancouver", + "country": "CA", + "latitude": 49.1902, + "longitude": -123.1837, + "metro": ["yvr01", "yvr"], + "roundrobin": true, + "site": "yvr01", + "uplink_speed": "1g" + }, + { + "city": "Winnipeg", + "country": "CA", + "latitude": 49.906, + "longitude": -97.2373, + "metro": ["ywg01", "ywg"], + "roundrobin": true, + "site": "ywg01", + "uplink_speed": "1g" + }, + { + "city": "Calgary", + "country": "CA", + "latitude": 51.1315, + "longitude": -114.0106, + "metro": ["yyc02", "yyc"], + "roundrobin": true, + "site": "yyc02", + "uplink_speed": "1g" + }, + { + "city": "Toronto", + "country": "CA", + "latitude": 43.6767, + "longitude": -79.6306, + "metro": ["yyz02", "yyz"], + "roundrobin": true, + "site": "yyz02", + "uplink_speed": "1g" + } ] diff --git a/components/measurement/nettests/mlab_utils.js b/components/measurement/nettests/mlab_utils.js index 8d7dffec7..427091996 100644 --- a/components/measurement/nettests/mlab_utils.js +++ b/components/measurement/nettests/mlab_utils.js @@ -3,7 +3,9 @@ const MLAB_SERVERS = require('./mlab_servers.json') import countryUtil from 'country-util' export const mlabServerDetails = (serverAddress, isNdt7) => { - const serverNode = isNdt7 ? serverAddress.split('-')[3].split('.')[0] : serverAddress.split('.')[3] + const serverNode = isNdt7 + ? serverAddress.split('-')[3].split('.')[0] + : serverAddress.split('.')[3] const server = MLAB_SERVERS.find((node) => node.site === serverNode) if (server) { diff --git a/components/measurement/useTestKeyController.js b/components/measurement/useTestKeyController.js deleted file mode 100644 index 16959facd..000000000 --- a/components/measurement/useTestKeyController.js +++ /dev/null @@ -1,72 +0,0 @@ -// This file contains a custom hook to render a toolbar -// that can be used to enable/disable parts of `testKeys` -// To use it, add the below code in components/measurement/MeasurementContainer.js -/* -import { useTestKeyController } from './useTestKeyController' -... -... -const { testKeys, TestKeyController } = useTestKeyController(measurement.test_keys) -const measurementMod = Object.assign({}, measurement, { test_keys: testKeys }) - -return ( - - - - -) -*/ - -import React, { useState, useCallback } from 'react' -import { Flex } from 'ooni-components' - - -const setValues = (input, value = true) => { - // Maps each key in `test_keys` to a boolean value, by default true - return Object.keys(input).reduce((o, k) => { - o[k] = value - return o - }, {all: value}) -} - -export const useTestKeyController = (testKeysInitial) => { - const [testKeys, setTestKeys] = useState(testKeysInitial) - const [keysMap, setKeysMap] = useState(setValues(testKeysInitial)) - - const TestKeyController = () => { - const onChange = useCallback((event) => { - const {name, checked} = event.target - - if (name === 'all') { - setTestKeys(checked ? testKeysInitial : {}) - setKeysMap(setValues(testKeysInitial, checked)) - } else { - const newTestKeys = {...testKeys} - // add or delete the original entry from `test_keys` - if (checked) { - newTestKeys[name] = testKeysInitial[name] - } else { - delete newTestKeys[name] - } - setTestKeys(newTestKeys) - setKeysMap(keysMap => { - const newKeysMap = {...keysMap} - newKeysMap[name] = checked - return newKeysMap - }) - } - }, [keysMap, testKeys, setKeysMap, setTestKeys]) - - return ( - - {Object.keys(keysMap).map((k, i) => - - - - - )} - - ) - } - - return { testKeys, TestKeyController } -} diff --git a/components/search/FilterSidebar.js b/components/search/FilterSidebar.js index 96aee6085..3880133d1 100644 --- a/components/search/FilterSidebar.js +++ b/components/search/FilterSidebar.js @@ -1,33 +1,28 @@ import { format } from 'date-fns' -import { Box, Button, Checkbox, Flex, Input, Label, RadioButton, RadioGroup, Select } from 'ooni-components' -import React, { useCallback, useEffect, useMemo, useState } from 'react' +import { + Checkbox, + Input, + RadioButton, + RadioGroup, + Select, +} from 'ooni-components' +import { useCallback, useEffect, useMemo, useState } from 'react' import { Controller, useForm } from 'react-hook-form' import { useIntl } from 'react-intl' import { TestNameOptions } from 'components/TestNameOptions' import { categoryCodes } from 'components/utils/categoryCodes' import dayjs from 'services/dayjs' -import styled from 'styled-components' import { getLocalisedRegionName } from 'utils/i18nCountries' import DateRangePicker from '../DateRangePicker' -const StyledLabel = styled(Label).attrs({ - mb: 1, - fontSize: 1, -})` - color: ${(props) => props.theme.colors.blue5}; - padding-top: 32px; -` - -const StyledDateRange = styled.div` - position: relative; -` - const CategoryOptions = () => { const intl = useIntl() return ( <> - + {categoryCodes .sort((a, b) => (a[1] < b[1] ? -1 : a[1] > b[1] ? 1 : 0)) .map(([code, label], idx) => ( @@ -73,7 +68,8 @@ function isValidFilterForTestname(testName = 'XX', arrayWithMapping) { const tomorrowUTC = dayjs.utc().add(1, 'day').format('YYYY-MM-DD') const asnRegEx = /^(AS)?([1-9][0-9]*)$/ -const domainRegEx = /(^[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,}(:[0-9]{1,5})?$)|(^(([0-9]{1,3})\.){3}([0-9]{1,3}))/ +const domainRegEx = + /(^[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,}(:[0-9]{1,5})?$)|(^(([0-9]{1,3})\.){3}([0-9]{1,3}))/ const inputRegEx = /(^https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,}\.[a-zA-Z0-9()]{2,}\b(?:[-a-zA-Z0-9()@:%_\+.~#?&\/=]*)$)|(^(([0-9]{1,3})\.){3}([0-9]{1,3}))/ @@ -119,7 +115,15 @@ const FilterSidebar = ({ hideFailed, } - const { handleSubmit, control, watch, resetField, formState, setValue, getValues } = useForm({ + const { + handleSubmit, + control, + watch, + resetField, + formState, + setValue, + getValues, + } = useForm({ defaultValues, }) const { errors } = formState @@ -130,7 +134,7 @@ const FilterSidebar = ({ // Does the selected testName need a domain filter const showDomain = useMemo( () => isValidFilterForTestname(testNameFilterValue, testsWithValidDomain), - [testNameFilterValue] + [testNameFilterValue], ) // to avoid bad queries, blank out the `domain` field when it is shown/hidden useEffect(() => { @@ -143,11 +147,12 @@ const FilterSidebar = ({ // Can we filter out anomalies or confirmed for this test_name const showAnomalyFilter = useMemo( () => isValidFilterForTestname(testNameFilterValue, testsWithAnomalyStatus), - [testNameFilterValue] + [testNameFilterValue], ) const showConfirmedFilter = useMemo( - () => isValidFilterForTestname(testNameFilterValue, testsWithConfirmedStatus), - [testNameFilterValue] + () => + isValidFilterForTestname(testNameFilterValue, testsWithConfirmedStatus), + [testNameFilterValue], ) // Reset status filter to 'all' if selected state isn't relevant // e.g 'anomalies' isn't relevant for `ndt`, or 'confirmed' for `telegram` @@ -169,19 +174,22 @@ const FilterSidebar = ({ const [showDatePicker, setShowDatePicker] = useState(false) - const handleRangeSelect = useCallback((range) => { - if (range?.from) { - setValue('sinceFilter', format(range.from, 'y-MM-dd')) - } else { - setValue('sinceFilter', '') - } - if (range?.to) { - setValue('untilFilter', format(range.to, 'y-MM-dd')) - } else { - setValue('untilFilter', '') - } - setShowDatePicker(false) - }, [setValue]) + const handleRangeSelect = useCallback( + (range) => { + if (range?.from) { + setValue('sinceFilter', format(range.from, 'y-MM-dd')) + } else { + setValue('sinceFilter', '') + } + if (range?.to) { + setValue('untilFilter', format(range.to, 'y-MM-dd')) + } else { + setValue('untilFilter', '') + } + setShowDatePicker(false) + }, + [setValue], + ) const countryOptions = useMemo(() => { const options = [ @@ -190,7 +198,7 @@ const FilterSidebar = ({ name: getLocalisedRegionName(c.alpha_2, intl.locale), })), ] - + options.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0)) options.unshift({ name: intl.formatMessage({ id: 'Search.Sidebar.Country.AllCountries' }), @@ -204,13 +212,13 @@ const FilterSidebar = ({
( - - - +
+
+
( setShowDatePicker(true)} onKeyDown={() => setShowDatePicker(false)} label={intl.formatMessage({ id: 'Search.Sidebar.From' })} - id='since-filter' + id="since-filter" mb={3} /> )} /> - - +
+
( setShowDatePicker(true)} onKeyDown={() => setShowDatePicker(false)} label={intl.formatMessage({ id: 'Search.Sidebar.Until' })} - id='until-filter' + id="until-filter" mb={3} /> )} /> - - +
+
{showDatePicker && ( setShowDatePicker(false)} /> )} - +
( @@ -331,17 +339,17 @@ const FilterSidebar = ({ <> ( )} @@ -354,17 +362,17 @@ const FilterSidebar = ({ /> ( )} rules={{ @@ -381,18 +389,20 @@ const FilterSidebar = ({ {(showConfirmedFilter || showAnomalyFilter) && ( <> - {intl.formatMessage({ id: 'Search.Sidebar.Status' })} + ( {showConfirmedFilter ? ( @@ -400,7 +410,7 @@ const FilterSidebar = ({ label={intl.formatMessage({ id: 'Search.FilterButton.Confirmed', })} - value='confirmed' + value="confirmed" mb={2} /> ) : ( @@ -411,7 +421,7 @@ const FilterSidebar = ({ label={intl.formatMessage({ id: 'Search.FilterButton.Anomalies', })} - value='anomalies' + value="anomalies" mb={2} /> ) : ( @@ -424,20 +434,20 @@ const FilterSidebar = ({ )} ( )} /> - + ) } diff --git a/components/search/FilterTabs.js b/components/search/FilterTabs.js deleted file mode 100644 index 0eed7557f..000000000 --- a/components/search/FilterTabs.js +++ /dev/null @@ -1,92 +0,0 @@ -import React from 'react' - -import styled from 'styled-components' -import { FormattedMessage } from 'react-intl' -import { - Flex, Box -} from 'ooni-components' - -const StyledFilterTab = styled.button` - font-size: 14px; - height: 32px; - text-transform: none; - padding: 0 16px; - display: inline-block; - line-height: 1; - vertical-align: middle; - // Gets rid of tap active state - -webkit-tap-highlight-color: transparent; - - outline: 0; - - // Specific - font-family: inherit; - font-weight: 600; - text-decoration: none; - - text-align: center; - letter-spacing: .5px; - z-index: 1; - transition: .2s ease-out; - cursor: pointer; - - background-color: ${props => props.active ? props.theme.colors.blue5 : 'transparent'}; - color: ${props => props.active ? props.theme.colors.white : props.theme.colors.blue5}; - &:active { - transition: .2s ease-in; - background-color: ${props => props.theme.colors.blue5}; - color: ${props => props.theme.colors.white}; - } - &:hover { - background-color: ${props => props.theme.colors.blue5}; - color: ${props => props.theme.colors.white}; - transition: .2s ease-in; - } -` - -const FilterTabLeft = styled(StyledFilterTab)` - border-radius: 32px 0px 0px 32px; - border: 1px solid ${props => props.theme.colors.blue5}; - border-right: 0px; -` -const FilterTabRight = styled(StyledFilterTab)` - border: 1px solid ${props => props.theme.colors.blue5}; - border-radius: 0px 32px 32px 0px; - border-left: 0px; -` - -const FilterTabCenter = styled(StyledFilterTab)` - border: 1px solid ${props => props.theme.colors.blue5}; - border-radius: 0px; -` - -const FilterTabs = ({onClick, onlyFilter}) => ( - - - {onClick('all')}} - active={onlyFilter === 'all'} - > - - - - - {onClick('confirmed')}} - active={onlyFilter === 'confirmed'} - > - - - - - {onClick('anomalies')}} - active={onlyFilter === 'anomalies'} - > - - - - -) - -export default FilterTabs diff --git a/components/search/Loader.js b/components/search/Loader.js index d18e5f120..65a1d039a 100644 --- a/components/search/Loader.js +++ b/components/search/Loader.js @@ -1,41 +1,39 @@ -import { theme } from 'ooni-components' +import { colors } from 'ooni-components' import PropTypes from 'prop-types' import ContentLoader from 'react-content-loader' export const LoaderRow = (props) => { - const isWide = (Math.random() * 100) > 70 + const isWide = Math.random() * 100 > 70 const random = Math.random() * (1 - 0.7) + 0.7 return ( - + {isWide && } ) } - export const Loader = ({ rows = 10 }) => ( <> {Array(rows) .fill('') .map((e, i) => ( - )) - } + ))} ) Loader.propTypes = { - rows: PropTypes.number + rows: PropTypes.number, } export default Loader diff --git a/components/search/ResultsList.js b/components/search/ResultsList.js index 283725393..86be5bf5e 100644 --- a/components/search/ResultsList.js +++ b/components/search/ResultsList.js @@ -1,14 +1,7 @@ -import NLink from 'next/link' -import { - Box, - Flex, - Text -} from 'ooni-components' +import Link from 'next/link' import PropTypes from 'prop-types' -import React from 'react' import { defineMessages, useIntl } from 'react-intl' import dayjs from 'services/dayjs' -import styled from 'styled-components' import url from 'url' import Flag from '../Flag' @@ -16,27 +9,10 @@ import { colorAnomaly, colorConfirmed, colorError, - colorNormal + colorNormal, } from '../colors' import { testNames } from '/components/test-info' -const StyledResultTag = styled.div` - border-radius: 16px; - padding: 4px 8px; - font-size: 12px; -` - -const ResultTagFilled = styled(StyledResultTag)` - background-color: ${props => props.theme.colors.gray7}; - color: ${props => props.theme.colors.white}; -` - -const ResultTagHollow = styled(StyledResultTag)` - background-color: transparent; - border: 1px solid ${props => props.theme.colors.gray7}; - color: ${props => props.theme.colors.gray7}; -` - const testsWithStates = [ 'web_connectivity', 'telegram', @@ -50,267 +26,223 @@ const testsWithStates = [ 'http_invalid_request_line', ] -const imTests = [ - 'telegram', - 'whatsapp', - 'facebook_messenger' -] +const imTests = ['telegram', 'whatsapp', 'facebook_messenger'] const messages = defineMessages({ 'Search.web_connectivity.Results.Reachable': { id: 'General.Accessible', - defaultMessage: '' + defaultMessage: '', }, 'Search.web_connectivity.Results.Anomaly': { id: 'General.Anomaly', - defaultMessage: '' + defaultMessage: '', }, 'Search.web_connectivity.Results.Blocked': { id: 'Search.WebConnectivity.Results.Blocked', - defaultMessage: '' + defaultMessage: '', }, 'Search.web_connectivity.Results.Error': { id: 'General.Error', - defaultMessage: '' + defaultMessage: '', }, 'Search.whatsapp.Results.Reachable': { id: 'General.Accessible', - defaultMessage: '' + defaultMessage: '', }, 'Search.whatsapp.Results.Anomaly': { id: 'General.Anomaly', - defaultMessage: '' + defaultMessage: '', }, 'Search.whatsapp.Results.Error': { id: 'General.Error', - defaultMessage: '' + defaultMessage: '', }, 'Search.facebook_messenger.Results.Reachable': { id: 'General.Accessible', - defaultMessage: '' + defaultMessage: '', }, 'Search.facebook_messenger.Results.Anomaly': { id: 'General.Anomaly', - defaultMessage: '' + defaultMessage: '', }, 'Search.facebook_messenger.Results.Error': { id: 'General.Error', - defaultMessage: '' + defaultMessage: '', }, 'Search.telegram.Results.Reachable': { id: 'General.Accessible', - defaultMessage: '' + defaultMessage: '', }, 'Search.telegram.Results.Anomaly': { id: 'General.Anomaly', - defaultMessage: '' + defaultMessage: '', }, 'Search.telegram.Results.Error': { id: 'General.Error', - defaultMessage: '' + defaultMessage: '', }, 'Search.signal.Results.Reachable': { id: 'General.Accessible', - defaultMessage: '' + defaultMessage: '', }, 'Search.signal.Results.Anomaly': { id: 'General.Anomaly', - defaultMessage: '' + defaultMessage: '', }, 'Search.signal.Results.Error': { id: 'General.Error', - defaultMessage: '' + defaultMessage: '', }, 'Search.http_invalid_request_line.Results.Anomaly': { id: 'General.Anomaly', - defaultMessage: '' + defaultMessage: '', }, 'Search.http_invalid_request_line.Results.Reachable': { id: 'General.OK', - defaultMessage: '' + defaultMessage: '', }, 'Search.http_invalid_request_line.Results.Error': { id: 'General.Error', - defaultMessage: '' + defaultMessage: '', }, 'Search.http_header_field_manipulation.Results.Anomaly': { id: 'General.Anomaly', - defaultMessage: '' + defaultMessage: '', }, 'Search.http_header_field_manipulation.Results.Reachable': { id: 'General.OK', - defaultMessage: '' + defaultMessage: '', }, 'Search.http_header_field_manipulation.Results.Error': { id: 'General.Error', - defaultMessage: '' + defaultMessage: '', }, 'Search.http_requests.Results.Reachable': { id: 'Search.HTTPRequests.Results.Reachable', - defaultMessage: '' + defaultMessage: '', }, 'Search.http_requests.Results.Error': { id: 'Search.HTTPRequests.Results.Error', - defaultMessage: '' + defaultMessage: '', }, 'Search.http_requests.Results.Blocked': { id: 'Search.HTTPRequests.Results.Blocked', - defaultMessage: '' + defaultMessage: '', }, 'Search.http_requests.Results.Anomaly': { id: 'Search.HTTPRequests.Results.Anomaly', - defaultMessage: '' + defaultMessage: '', }, 'Search.tor.Results.Reachable': { id: 'General.OK', - defaultMessage: '' + defaultMessage: '', }, 'Search.tor.Results.Anomaly': { id: 'General.Anomaly', - defaultMessage: '' + defaultMessage: '', }, 'Search.tor.Results.Error': { id: 'General.Error', - defaultMessage: '' + defaultMessage: '', }, 'Search.torsf.Results.Reachable': { id: 'General.OK', - defaultMessage: 'Reachable' + defaultMessage: 'Reachable', }, 'Search.torsf.Results.Anomaly': { id: 'General.Anomaly', - defaultMessage: 'Anomaly' + defaultMessage: 'Anomaly', }, 'Search.torsf.Results.Error': { id: 'General.Error', - defaultMessage: 'Anomaly' + defaultMessage: 'Anomaly', }, 'Search.psiphon.Results.Reachable': { id: 'General.OK', - defaultMessage: '' + defaultMessage: '', }, 'Search.psiphon.Results.Anomaly': { id: 'General.Anomaly', - defaultMessage: '' + defaultMessage: '', }, 'Search.psiphon.Results.Error': { id: 'General.Error', - defaultMessage: '' + defaultMessage: '', }, 'Search.riseupvpn.Results.Reachable': { id: 'General.Accessible', - defaultMessage: '' + defaultMessage: '', }, 'Search.riseupvpn.Results.Anomaly': { id: 'General.Anomaly', - defaultMessage: '' + defaultMessage: '', }, 'Search.riseupvpn.Results.Error': { id: 'General.Error', - defaultMessage: '' + defaultMessage: '', }, }) -const ASNBox = ({asn}) => { +const ASNBox = ({ asn }) => { const justNumber = asn.split('AS')[1] - return AS {justNumber} + return
AS {justNumber}
} ASNBox.propTypes = { - asn: PropTypes.string + asn: PropTypes.string, } -const StyledViewDetailsLink = styled(NLink)` - cursor: pointer; - text-decoration: none; - color: ${props => props.theme.colors.blue5}; - &:hover { - color: ${props => props.theme.colors.blue9}; - } -` - -const ViewDetailsLink = ({measurementUid, children}) => { - let href = `/m/${measurementUid}` - - return ( - {children} - ) -} - -ViewDetailsLink.propTypes = { - measurementUid: PropTypes.string, - children: PropTypes.element.isRequired -} +const tagClassNames = 'rounded-2xl py-1 px-2 text-xs' +const hollowTagClasNames = `${tagClassNames} bg-transparent border border-gray-700` -const ColoredIndicator = styled.div` - height: 100%; - width: 5px; - margin-right: 10px; - background-color: ${props => props.color || 'unset'} -` - -const ResultRow = styled(Flex)` - color: ${props => props.theme.colors.gray7}; - background-color: #ffffff; - &:hover { - background-color: ${props => props.theme.colors.gray0}; - } - border-bottom: 1px solid ${props => props.theme.colors.gray4}; - cursor: pointer; -` - -const Hostname = styled.span` - color: ${props => props.theme.colors.black}; -` - -const ResultInput = styled.div` - color: ${props => props.theme.colors.gray5}; -` - -const getIndicators = ({ test_name, scores = {}, confirmed, anomaly, failure, intl }) => { - let color = '', tag = null +const getIndicators = ({ + test_name, + scores = {}, + confirmed, + anomaly, + failure, + intl, +}) => { + let color = '' + let tag = null if (testsWithStates.includes(test_name)) { if (imTests.includes(test_name) && Object.entries(scores).length === 0) { return [color, tag] } const computedMessageIdPrefix = `Search.${test_name}.Results` - const blockingType = scores.analysis && scores.analysis.blocking_type + const blockingType = scores.analysis?.blocking_type if (failure === true) { color = colorError tag = ( - +
{intl.formatMessage(messages[`${computedMessageIdPrefix}.Error`])} - +
) } else if (confirmed === true) { color = colorConfirmed tag = ( - +
{intl.formatMessage(messages[`${computedMessageIdPrefix}.Blocked`])} - +
) } else if (blockingType !== undefined) { color = colorAnomaly - tag = ( - - {blockingType} - - ) + tag =
{blockingType}
} else if (anomaly === true) { color = colorAnomaly tag = ( - +
{intl.formatMessage(messages[`${computedMessageIdPrefix}.Anomaly`])} - +
) } else { color = colorNormal tag = ( - +
{intl.formatMessage(messages[`${computedMessageIdPrefix}.Reachable`])} - +
) } } @@ -327,7 +259,7 @@ const ResultItem = ({ scores, confirmed, anomaly, - failure + failure, }) => { const intl = useIntl() const pathMaxLen = 10 @@ -348,60 +280,72 @@ const ResultItem = ({ p.host = `${p.host.substr(0, domainMaxLen)}…` } - inputLabel = {`${p.protocol}//${p.host}`}{path} + inputLabel = ( + + {`${p.protocol}//${p.host}`} + {path} + + ) } else { - inputLabel = {p.path} + inputLabel = {p.path} } } - const [indicatorColor, tag] = getIndicators({test_name, scores, confirmed, anomaly, failure, intl}) + const [indicatorColor, tag] = getIndicators({ + test_name, + scores, + confirmed, + anomaly, + failure, + intl, + }) const testName = testNames[test_name]?.name || test_name return ( - - - - - - - - - - - {probe_cc} - - + +
+
+
+
+
+
+
+
+
+
{probe_cc}
+
+
- - +
+
- - - {dayjs.utc(measurement_start_time).format('YYYY-MM-DD HH:mm [UTC]')} - - - {testName} - - - - - - - - {input && - - {inputLabel} - } - - - {tag} - - - - - - - +
+
+ {dayjs + .utc(measurement_start_time) + .format('YYYY-MM-DD HH:mm [UTC]')} +
+
{testName}
+
+
+ +
+
+ {input && ( +
+ {inputLabel} +
+ )} +
{tag}
+
+
+
+
+
+ ) } @@ -418,21 +362,18 @@ ResultItem.propTypes = { failure: PropTypes.bool, } -const ResultContainer = styled(Box)` - border: 1px solid ${props => props.theme.colors.gray4}; - border-radius: 5px; - overflow: hidden; -` - const ResultsList = ({ results }) => { return ( - - +
+
{results.map((msmt, idx) => { return })} - - +
+
) } diff --git a/components/test-info.js b/components/test-info.js index 5805caff5..e32ea7fce 100644 --- a/components/test-info.js +++ b/components/test-info.js @@ -1,235 +1,230 @@ -import React from 'react' +import { colors } from 'ooni-components' import { - theme -} from 'ooni-components' - -import { FaBeer } from 'react-icons/fa' -import { - NettestGroupWebsites, + NettestGroupCircumvention, + NettestGroupExperimental, NettestGroupInstantMessaging, NettestGroupMiddleBoxes, NettestGroupPerformance, - NettestGroupCircumvention, - NettestGroupExperimental + NettestGroupWebsites, } from 'ooni-components/icons' +import { FaBeer } from 'react-icons/fa' import { FormattedMessage } from 'react-intl' - export const testGroups = { - 'websites': { - 'color': theme.colors.indigo7, - 'id': 'Tests.Groups.Websites.Name', - 'name': , - 'icon': - }, - 'im': { - 'color': theme.colors.cyan7, - 'id': 'Tests.Groups.Instant Messagging.Name', - 'name': , - 'icon': - }, - 'middlebox': { - 'color': theme.colors.violet9, - 'id': 'Tests.Groups.Middlebox.Name', - 'name': , - 'icon': - }, - 'performance': { - 'color': theme.colors.fuchsia6, - 'id': 'Tests.Groups.Performance.Name', - 'name': , - 'icon': - }, - 'circumvention': { - 'color': theme.colors.pink6, - 'id': 'Tests.Groups.Circumvention.Name', - 'name': , - 'icon': - }, - 'experimental': { - 'color': theme.colors.gray5, - 'id': 'Tests.Groups.Experimental.Name', - 'name': , - 'icon': - }, - 'legacy': { - 'color': theme.colors.gray5, - 'id': 'Tests.Groups.Legacy.Name', - 'name': , - 'icon': - }, - 'default': { - 'color': theme.colors.gray5, - 'id': 'DefaultTestGroupName', - 'name': null, - 'icon': + websites: { + color: colors.indigo['700'], + id: 'Tests.Groups.Websites.Name', + name: , + icon: , + }, + im: { + color: colors.cyan['700'], + id: 'Tests.Groups.Instant Messagging.Name', + name: , + icon: , + }, + middlebox: { + color: colors.violet['900'], + id: 'Tests.Groups.Middlebox.Name', + name: , + icon: , + }, + performance: { + color: colors.fuchsia['600'], + id: 'Tests.Groups.Performance.Name', + name: , + icon: , + }, + circumvention: { + color: colors.pink['600'], + id: 'Tests.Groups.Circumvention.Name', + name: , + icon: , + }, + experimental: { + color: colors.gray['500'], + id: 'Tests.Groups.Experimental.Name', + name: , + icon: , + }, + legacy: { + color: colors.gray['500'], + id: 'Tests.Groups.Legacy.Name', + name: , + icon: , + }, + default: { + color: colors.gray['500'], + id: 'DefaultTestGroupName', + name: null, + icon: , }, } export const testNames = { /* Websites */ - 'web_connectivity': { + web_connectivity: { group: 'websites', - name: , + name: , id: 'Tests.WebConnectivity.Name', - info: 'https://ooni.org/nettest/web-connectivity/' + info: 'https://ooni.org/nettest/web-connectivity/', }, /* Middlebox tests */ - 'http_invalid_request_line': { + http_invalid_request_line: { group: 'middlebox', - name: , + name: , id: 'Tests.HTTPInvalidReqLine.Name', - info: 'https://ooni.org/nettest/http-invalid-request-line/' + info: 'https://ooni.org/nettest/http-invalid-request-line/', }, - 'http_header_field_manipulation': { + http_header_field_manipulation: { group: 'middlebox', - name: , + name: , id: 'Tests.HTTPHeaderManipulation.Name', - info: 'https://ooni.org/nettest/http-header-field-manipulation/' + info: 'https://ooni.org/nettest/http-header-field-manipulation/', }, /* IM Tests */ - 'facebook_messenger': { + facebook_messenger: { group: 'im', - name: , + name: , id: 'Tests.Facebook.Name', - info: 'https://ooni.org/nettest/facebook-messenger/' + info: 'https://ooni.org/nettest/facebook-messenger/', }, - 'telegram': { + telegram: { group: 'im', - name: , + name: , id: 'Tests.Telegram.Name', - info: 'https://ooni.org/nettest/telegram/' + info: 'https://ooni.org/nettest/telegram/', }, - 'whatsapp': { + whatsapp: { group: 'im', - name: , + name: , id: 'Tests.WhatsApp.Name', - info: 'https://ooni.org/nettest/whatsapp/' + info: 'https://ooni.org/nettest/whatsapp/', }, - 'signal': { + signal: { group: 'im', - name: , + name: , id: 'Tests.Signal.Name', - info: 'https://ooni.org/nettest/signal/' + info: 'https://ooni.org/nettest/signal/', }, /* Performance */ - 'ndt': { + ndt: { group: 'performance', - name: , + name: , id: 'Tests.NDT.Name', - info: 'https://ooni.org/nettest/ndt/' + info: 'https://ooni.org/nettest/ndt/', }, - 'dash': { + dash: { group: 'performance', - name: , + name: , id: 'Tests.Dash.Name', - info: 'https://ooni.org/nettest/dash/' + info: 'https://ooni.org/nettest/dash/', }, /* Censorship circumvention */ - 'bridge_reachability': { + bridge_reachability: { group: 'legacy', - name: , + name: , id: 'Tests.BridgeReachability.Name', - info: 'https://ooni.org/nettest/tor-bridge-reachability/' + info: 'https://ooni.org/nettest/tor-bridge-reachability/', }, - 'psiphon': { + psiphon: { group: 'circumvention', - name: , + name: , id: 'Tests.Psiphon.Name', - info: 'https://ooni.org/nettest/psiphon/' + info: 'https://ooni.org/nettest/psiphon/', }, - 'tor': { + tor: { group: 'circumvention', - name: , + name: , id: 'Tests.Tor.Name', - info: 'https://ooni.org/nettest/tor/' + info: 'https://ooni.org/nettest/tor/', }, - 'torsf': { + torsf: { group: 'circumvention', - name: , + name: , id: 'Tests.TorSnowflake.Name', - info: 'https://ooni.org/nettest/torsf/' + info: 'https://ooni.org/nettest/torsf/', }, - 'riseupvpn': { + riseupvpn: { group: 'experimental', - name: , + name: , id: 'Tests.RiseupVPN.Name', - info: 'https://ooni.org/nettest/' + info: 'https://ooni.org/nettest/', }, /* Legacy tests */ - 'tcp_connect': { + tcp_connect: { group: 'legacy', - name: , + name: , id: 'Tests.TCPConnect.Name', // FIXME: Use a more relevant link - info: 'https://ooni.org/nettest/' + info: 'https://ooni.org/nettest/', }, - 'dns_consistency': { + dns_consistency: { group: 'legacy', - name: , + name: , id: 'Tests.DNSConsistency.Name', - info: 'https://ooni.org/nettest/dns-consistency/' + info: 'https://ooni.org/nettest/dns-consistency/', }, - 'http_requests': { + http_requests: { group: 'legacy', - name: , + name: , id: 'Tests.HTTPRequests.Name', - info: 'https://ooni.org/nettest/http-requests/' + info: 'https://ooni.org/nettest/http-requests/', }, - 'http_host': { + http_host: { group: 'legacy', - name: , + name: , id: 'Tests.HTTPHost.Name', - info: 'https://ooni.org/nettest/http-host/' + info: 'https://ooni.org/nettest/http-host/', }, - 'meek_fronted_requests_test': { + meek_fronted_requests_test: { group: 'legacy', - name: , + name: , id: 'Tests.MeekFrontendRequests.Name', - info: 'https://ooni.org/nettest/meek-fronted-requests/' + info: 'https://ooni.org/nettest/meek-fronted-requests/', }, - 'multi_protocol_traceroute': { + multi_protocol_traceroute: { group: 'legacy', - name: , + name: , id: 'Tests.MultiProtocolTraceroute.Name', - info: 'https://ooni.org/nettest/' + info: 'https://ooni.org/nettest/', }, /* Experimental tests */ - 'vanilla_tor': { + vanilla_tor: { group: 'experimental', - name: , + name: , id: 'Tests.TorVanilla.Name', - info: 'https://ooni.org/nettest/vanilla-tor/' + info: 'https://ooni.org/nettest/vanilla-tor/', }, - 'dnscheck': { + dnscheck: { group: 'experimental', - name: , + name: , id: 'Tests.DNSCheck.Name', - info: 'https://ooni.org/nettest/http-requests/' + info: 'https://ooni.org/nettest/http-requests/', }, - 'stunreachability': { + stunreachability: { group: 'experimental', - name: , + name: , id: 'Tests.StunReachability.Name', - info: 'https://ooni.org/nettest/http-requests/' + info: 'https://ooni.org/nettest/http-requests/', }, - 'urlgetter': { + urlgetter: { group: 'experimental', - name: , + name: , id: 'Tests.URLGetter.Name', - info: 'https://ooni.org/nettest/http-requests/' + info: 'https://ooni.org/nettest/http-requests/', }, - 'browser_web': { + browser_web: { group: 'experimental', - name: , + name: , id: 'Tests.ProbeWeb.Name', - info: 'https://github.com/ooni/spec/blob/master/nettests/ts-036-browser_web.md' - } + info: 'https://github.com/ooni/spec/blob/master/nettests/ts-036-browser_web.md', + }, } diff --git a/components/useMobileDetection.js b/components/useMobileDetection.js deleted file mode 100644 index fad5e609f..000000000 --- a/components/useMobileDetection.js +++ /dev/null @@ -1,13 +0,0 @@ -import { useState, useEffect } from 'react' - -export const useMobileDetection = () => { - const [isMobile, setIsMobile] = useState(false) - - // Equivalent of componentDidMount and componentDidUpdate - // Limited to run only once by passing `[]` as second argument - useEffect(() => { - setIsMobile(window.innerWidth < 800) - }, []) - - return isMobile -} diff --git a/components/utils.js b/components/utils.js index 1a5525f59..4b203aa3d 100644 --- a/components/utils.js +++ b/components/utils.js @@ -1,12 +1,12 @@ import { testGroups, testNames } from './test-info' export const getTestMetadata = (testName) => { - let metadata = { - 'name': testName, - 'groupName': testGroups.default.name, - 'color': testGroups.default.color, - 'icon': testGroups.default.icon, - 'info': 'https://ooni.org/nettest/' + const metadata = { + name: testName, + groupName: testGroups.default.name, + color: testGroups.default.color, + icon: testGroups.default.icon, + info: 'https://ooni.org/nettest/', } const test = testNames[testName] @@ -14,10 +14,10 @@ export const getTestMetadata = (testName) => { return metadata } const group = testGroups[test.group] - metadata['name'] = test.name - metadata['groupName'] = group.name - metadata['icon'] = group.icon - metadata['color'] = group.color - metadata['info'] = test.info + metadata.name = test.name + metadata.groupName = group.name + metadata.icon = group.icon + metadata.color = group.color + metadata.info = test.info return metadata } diff --git a/components/utils/categoryCodes.js b/components/utils/categoryCodes.js index ab9093089..92dd94e7d 100644 --- a/components/utils/categoryCodes.js +++ b/components/utils/categoryCodes.js @@ -9,156 +9,142 @@ export const categoryCodes = [ [ 'ALDR', 'Alcohol & Drugs', - 'Sites devoted to the use, paraphernalia, and sale of drugs and alcohol irrespective of the local legality.' + 'Sites devoted to the use, paraphernalia, and sale of drugs and alcohol irrespective of the local legality.', ], [ 'ANON', 'Anonymization and circumvention tools', - 'Sites that provide tools used for anonymization, circumvention, proxy-services and encryption.' + 'Sites that provide tools used for anonymization, circumvention, proxy-services and encryption.', ], [ 'COMT', 'Communication Tools', - 'Sites and tools for individual and group communications. Includes webmail, VoIP, instant messaging, chat and mobile messaging applications.' - ], - [ - 'CTRL', - 'Control content', - 'Benign or innocuous content used as a control.' + 'Sites and tools for individual and group communications. Includes webmail, VoIP, instant messaging, chat and mobile messaging applications.', ], + ['CTRL', 'Control content', 'Benign or innocuous content used as a control.'], [ 'CULTR', 'Culture', - 'Content relating to entertainment, history, literature, music, film, books, satire and humour' - ], - [ - 'COMM', - 'E-commerce', - 'Websites of commercial services and products.' + 'Content relating to entertainment, history, literature, music, film, books, satire and humour', ], + ['COMM', 'E-commerce', 'Websites of commercial services and products.'], [ 'ECON', 'Economics', - 'General economic development and poverty related topics, agencies and funding opportunities' + 'General economic development and poverty related topics, agencies and funding opportunities', ], [ 'ENV', 'Environment', - 'Pollution, international environmental treaties, deforestation, environmental justice, disasters, etc.' + 'Pollution, international environmental treaties, deforestation, environmental justice, disasters, etc.', ], [ 'FILE', 'File-sharing', - 'Sites and tools used to share files, including cloud-based file storage, torrents and P2P file-sharing tools.' + 'Sites and tools used to share files, including cloud-based file storage, torrents and P2P file-sharing tools.', ], [ 'GMB', 'Gambling', - 'Online gambling sites. Includes casino games, sports betting, etc.' + 'Online gambling sites. Includes casino games, sports betting, etc.', ], [ 'GAME', 'Gaming', - 'Online games and gaming platforms, excluding gambling sites.' - ], - [ - 'GOVT', - 'Government', - 'Government-run websites, including military sites.' + 'Online games and gaming platforms, excluding gambling sites.', ], + ['GOVT', 'Government', 'Government-run websites, including military sites.'], [ 'HACK', 'Hacking Tools', - 'Sites dedicated to computer security, including news and tools. Includes malicious and non-malicious content.' + 'Sites dedicated to computer security, including news and tools. Includes malicious and non-malicious content.', ], [ 'HATE', 'Hate Speech', - 'Content that disparages particular groups or persons based on race, sex, sexuality or other characteristics' + 'Content that disparages particular groups or persons based on race, sex, sexuality or other characteristics', ], [ 'HOST', 'Hosting and Blogging Platforms', - 'Web hosting services, blogging and other online publishing platforms.' + 'Web hosting services, blogging and other online publishing platforms.', ], [ 'HUMR', 'Human Rights Issues', - "Sites dedicated to discussing human rights issues in various forms. Includes women's rights and rights of minority ethnic groups." + "Sites dedicated to discussing human rights issues in various forms. Includes women's rights and rights of minority ethnic groups.", ], [ 'IGO', 'Intergovernmental Organizations', - 'Websites of intergovernmental organizations such as the United Nations.' + 'Websites of intergovernmental organizations such as the United Nations.', ], [ 'LGBT', 'LGBT', - 'A range of gay-lesbian-bisexual-transgender queer issues. (Excluding pornography)' - ], - [ - 'MMED', - 'Media sharing', - 'Video, audio or photo sharing platforms.' + 'A range of gay-lesbian-bisexual-transgender queer issues. (Excluding pornography)', ], + ['MMED', 'Media sharing', 'Video, audio or photo sharing platforms.'], [ 'MISC', 'Miscelaneous content', - "Sites that don't fit in any category (XXX Things in here should be categorised)" + "Sites that don't fit in any category (XXX Things in here should be categorised)", ], [ 'NEWS', 'News Media', - 'This category includes major news outlets (BBC, CNN, etc.) as well as regional news outlets and independent media.' + 'This category includes major news outlets (BBC, CNN, etc.) as well as regional news outlets and independent media.', ], [ 'DATE', 'Online Dating', - 'Online dating services which can be used to meet people, post profiles, chat, etc' + 'Online dating services which can be used to meet people, post profiles, chat, etc', ], [ 'POLR', 'Political Criticism', - 'Content that offers critical political viewpoints. Includes critical authors and bloggers, as well as oppositional political organizations. Includes pro-democracy content, anti-corruption content as well as content calling for changes in leadership, governance issues, legal reform. Etc.' + 'Content that offers critical political viewpoints. Includes critical authors and bloggers, as well as oppositional political organizations. Includes pro-democracy content, anti-corruption content as well as content calling for changes in leadership, governance issues, legal reform. Etc.', ], - [ 'PORN', 'Pornography', 'Hard-core and soft-core pornography.' ], + ['PORN', 'Pornography', 'Hard-core and soft-core pornography.'], [ 'PROV', 'Provocative Attire', - 'Websites which show provocative attire and portray women in a sexual manner, wearing minimal clothing.' + 'Websites which show provocative attire and portray women in a sexual manner, wearing minimal clothing.', ], [ 'PUBH', 'Public Health', - 'HIV, SARS, bird flu, centers for disease control, World Health Organization, etc' + 'HIV, SARS, bird flu, centers for disease control, World Health Organization, etc', ], [ 'REL', 'Religion', - 'Sites devoted to discussion of religious issues, both supportive and critical, as well as discussion of minority religious groups.' + 'Sites devoted to discussion of religious issues, both supportive and critical, as well as discussion of minority religious groups.', ], - [ 'SRCH', 'Search Engines', 'Search engines and portals.' ], + ['SRCH', 'Search Engines', 'Search engines and portals.'], [ 'XED', 'Sex Education', - 'Includes contraception, abstinence, STDs, healthy sexuality, teen pregnancy, rape prevention, abortion, sexual rights, and sexual health services.' - ], - [ - 'GRP', - 'Social Networking', - 'Social networking tools and platforms.' + 'Includes contraception, abstinence, STDs, healthy sexuality, teen pregnancy, rape prevention, abortion, sexual rights, and sexual health services.', ], + ['GRP', 'Social Networking', 'Social networking tools and platforms.'], [ 'MILX', 'Terrorism and Militants', - 'Sites promoting terrorism, violent militant or separatist movements.' - ] + 'Sites promoting terrorism, violent militant or separatist movements.', + ], ] /* eslint-enable quotes */ export const getCategoryCodesMap = () => { - const map = categoryCodes.reduce((acc, [code, name, description]) => - acc.set(code, {code, name: `CategoryCode.${code}.Name`, description: `CategoryCode.${code}.Description`}) - , new Map()) + const map = categoryCodes.reduce( + (acc, [code, name, description]) => + acc.set(code, { + code, + name: `CategoryCode.${code}.Name`, + description: `CategoryCode.${code}.Description`, + }), + new Map(), + ) return map } diff --git a/components/utils/profiler.js b/components/utils/profiler.js index 03272a562..834af2ea5 100644 --- a/components/utils/profiler.js +++ b/components/utils/profiler.js @@ -2,7 +2,7 @@ import { Profiler as NativeProfiler } from 'react' export const Profiler = ({ id, children }) => { const _id = `${id || children.type.type.displayName}.${children.key}` - + return ( {children} @@ -10,11 +10,20 @@ export const Profiler = ({ id, children }) => { ) } -export function profilerLog(id, phase, actualTime, baseTime, startTime, commitTime, interactions) { +export function profilerLog( + id, + phase, + actualTime, + baseTime, + startTime, + commitTime, + interactions, +) { console.debug(`${id}: ${phase}: ${actualTime}`) // console.debug(`actualTime: ${actualTime}, baseTime: ${baseTime}, startTime, commitTime`) const columns = [ - 'id', 'phase', + 'id', + 'phase', 'actualTime', 'baseTime', // 'startTime', @@ -22,4 +31,4 @@ export function profilerLog(id, phase, actualTime, baseTime, startTime, commitTi // 'interactions' ] // console.table([{id, phase, actualTime, baseTime, startTime, commitTime, interactions}], columns) -} \ No newline at end of file +} diff --git a/components/vendor/SpinLoader.js b/components/vendor/SpinLoader.js index fc587b685..0a2d1b963 100644 --- a/components/vendor/SpinLoader.js +++ b/components/vendor/SpinLoader.js @@ -1,80 +1,49 @@ // From: https://github.com/LucasBassetti/react-css-loaders/tree/master/lib/spin -import React from 'react' -import PropTypes from 'prop-types' -import styled, { css, keyframes } from 'styled-components' -import { theme } from 'ooni-components' -const loading = keyframes` - 0% { - transform: rotate(0deg); - } - 100% { - transform: rotate(360deg); - } -` - -const spinnerAnimation = props => - css` - ${loading} ${props.$duration}s infinite linear; - ` - -const Spin = styled.div` - animation: ${spinnerAnimation}; - background: ${props => props.$color}; - background: ${props => `linear-gradient(to right, ${props.$color} 10%, rgba(255, 255, 255, 0) 42%);`}; - border-radius: 50%; - font-size: ${props => `${props.$size}px`}; - height: 11em; - margin: ${props => props.$margin}; - position: relative; - text-indent: -9999em; - transform: translateZ(0); - width: 11em; - - &:before { - background: ${props => props.$color}; - border-radius: 100% 0 0 0; - content: ''; - height: 50%; - left: 0; - position: absolute; - top: 0; - width: 50%; - } - - &:after { - background: ${props => props.$background}; - border-radius: 50%; - bottom: 0; - content: ''; - height: 75%; - left: 0; - margin: auto; - position: absolute; - right: 0; - top: 0; - width: 75%; - } -` - -const SpinLoader = props => ( - +import { twMerge } from 'tailwind-merge' + +const getClassName = (classes) => + twMerge( + `h-12 + w-12 + animate-spin + bg-white + text-blue-500 + duration-150 + my-[50px] + mx-auto + rounded-[50%] + text-base + bg-gradient-to-r + from-blue-500 + from-10% + to-transparent + to-45% + before:content-[''] + before:bg-blue-500 + before:rounded-[100%_0_0_0] + before:h-[50%] + before:w-[50%] + before:left-0 + before:top-0 + before:absolute + after:content-[''] + after:bg-white + after:rounded-[50%] + after:bottom-0 + after:top-0 + after:left-0 + after:right-0 + after:m-auto + after:h-[75%] + after:w-[75%] + after:absolute + `, + classes, + ) + +const SpinLoader = ({ className, props }) => ( +
) -SpinLoader.propTypes = { - $background: PropTypes.string, - $color: PropTypes.string, - $duration: PropTypes.number, - $size: PropTypes.number, - $margin: PropTypes.string, -} - -SpinLoader.defaultProps = { - $background: '#fff', - $color: theme.colors.blue5, - $duration: 1.4, - $size: 5, - $margin: '50px auto', -} - export default SpinLoader diff --git a/components/withIntl.js b/components/withIntl.js index cfb2175ee..7e91d3bba 100644 --- a/components/withIntl.js +++ b/components/withIntl.js @@ -1,9 +1,9 @@ /* global require */ -import React, { useMemo } from 'react' -import { IntlProvider } from 'react-intl' import { useRouter } from 'next/router' +import { useMemo } from 'react' +import { IntlProvider } from 'react-intl' -export const getDirection = locale => { +export const getDirection = (locale) => { switch (locale) { case 'fa': case 'ar': @@ -19,19 +19,23 @@ export const LocaleProvider = ({ children }) => { const messages = useMemo(() => { try { const messages = require(`../public/static/lang/${locale}.json`) - const defaultMessages = require(`../public/static/lang/${defaultLocale}.json`) + const defaultMessages = require( + `../public/static/lang/${defaultLocale}.json`, + ) const mergedMessages = Object.assign({}, defaultMessages, messages) return mergedMessages } catch (e) { console.error(`Failed to load messages for ${locale}: ${e.message}`) - const defaultMessages = require(`../public/static/lang/${defaultLocale}.json`) + const defaultMessages = require( + `../public/static/lang/${defaultLocale}.json`, + ) return defaultMessages } }, [locale, defaultLocale]) return ( - { const [searchValue, setSearchValue] = useState('') const [categoryValue, setCategoryValue] = useState('') const [sortValue, setSortValue] = useState(initialSortValue) - + const searchHandler = (searchTerm) => { setSearchValue(searchTerm) } - const debouncedSearchHandler = useMemo(() => debounce(searchHandler, 200),[]) - + const debouncedSearchHandler = useMemo(() => debounce(searchHandler, 200), []) + return { searchValue, sortValue, @@ -22,4 +22,4 @@ const useFilterWithSort = ({ initialSortValue, initialFilterValue = '' }) => { } } -export default useFilterWithSort \ No newline at end of file +export default useFilterWithSort diff --git a/hooks/useScrollPosition.js b/hooks/useScrollPosition.js index d1279478c..489d40631 100644 --- a/hooks/useScrollPosition.js +++ b/hooks/useScrollPosition.js @@ -1,4 +1,4 @@ -import { useState, useEffect } from 'react' +import { useEffect, useState } from 'react' const useScrollPosition = () => { const [scrollPosition, setScrollPosition] = useState(0) @@ -15,4 +15,4 @@ const useScrollPosition = () => { return scrollPosition } -export default useScrollPosition \ No newline at end of file +export default useScrollPosition diff --git a/hooks/useUser.js b/hooks/useUser.js index 29e2a0b02..7e5952bbc 100644 --- a/hooks/useUser.js +++ b/hooks/useUser.js @@ -1,5 +1,12 @@ import { useRouter } from 'next/router' -import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react' +import { + createContext, + useCallback, + useContext, + useEffect, + useMemo, + useState, +} from 'react' import { apiEndpoints, getAPI, loginUser, refreshToken } from '/lib/api' @@ -29,7 +36,7 @@ export const UserProvider = ({ children }) => { router.push({ pathname, query: Object.fromEntries([...searchParams]) }) }, 3000) }, - [router] + [router], ) useEffect(() => { @@ -52,7 +59,9 @@ export const UserProvider = ({ children }) => { // new one if needed useEffect(() => { const interval = setInterval(() => { - const tokenCreatedAt = JSON.parse(localStorage.getItem('bearer'))?.created_at + const tokenCreatedAt = JSON.parse( + localStorage.getItem('bearer'), + )?.created_at if (tokenCreatedAt) { const tokenExpiry = tokenCreatedAt + TWELVE_HOURS const now = Date.now() @@ -101,10 +110,12 @@ export const UserProvider = ({ children }) => { login, logout, }), - [user, loading, error] + [user, loading, error], ) - return {children} + return ( + {children} + ) } const useUser = () => { diff --git a/lib/api.js b/lib/api.js index 1ee9eedf8..d0ad54c0f 100644 --- a/lib/api.js +++ b/lib/api.js @@ -28,7 +28,9 @@ export const getUserEmail = () => { : '' } -const axios = Axios.create({ baseURL: process.env.NEXT_PUBLIC_USER_FEEDBACK_API }) +const axios = Axios.create({ + baseURL: process.env.NEXT_PUBLIC_USER_FEEDBACK_API, +}) export const getAPI = async (endpoint, params = {}, config = {}) => { const bearerToken = getBearerToken() @@ -38,7 +40,9 @@ export const getAPI = async (endpoint, params = {}, config = {}) => { url: endpoint, params: params, ...config, - ...(bearerToken && { headers: { Authorization: `Bearer ${bearerToken}` } }), + ...(bearerToken && { + headers: { Authorization: `Bearer ${bearerToken}` }, + }), }) .then((res) => res.data) .catch((e) => { @@ -53,11 +57,15 @@ const postAPI = async (endpoint, params, config) => { return await getAPI(endpoint, null, { method: 'POST', data: params }) } -export const registerUser = async (email_address, redirectUrl = 'https://explorer.ooni.org') => { +export const registerUser = async ( + email_address, + redirectUrl = 'https://explorer.ooni.org', +) => { // current testing setup does not enable us to check process.env.NODE_ENV (it's set to production // in headless mode), therefore custom NEXT_PUBLIC_IS_TEST_ENV is used const redirectTo = - process.env.NODE_ENV === 'development' || process.env.NEXT_PUBLIC_IS_TEST_ENV + process.env.NODE_ENV === 'development' || + process.env.NEXT_PUBLIC_IS_TEST_ENV ? 'https://explorer.test.ooni.org' : redirectUrl @@ -73,10 +81,19 @@ export const submitFeedback = (feedback) => { } export const loginUser = (token) => { - return axios.get(apiEndpoints.USER_LOGIN, { params: { k: token } }).then(({ data }) => { - localStorage.setItem('bearer', JSON.stringify({ token: data?.bearer, email_address: data?.email_address, created_at: Date.now() })) - return data - }) + return axios + .get(apiEndpoints.USER_LOGIN, { params: { k: token } }) + .then(({ data }) => { + localStorage.setItem( + 'bearer', + JSON.stringify({ + token: data?.bearer, + email_address: data?.email_address, + created_at: Date.now(), + }), + ) + return data + }) } export const createIncidentReport = (report) => { @@ -102,7 +119,14 @@ export const unpublishIncidentReport = (report) => { export const refreshToken = () => { const email_address = getUserEmail() return getAPI(apiEndpoints.TOKEN_REFRESH).then((data) => { - localStorage.setItem('bearer', JSON.stringify({ token: data.bearer, email_address, created_at: Date.now() })) + localStorage.setItem( + 'bearer', + JSON.stringify({ + token: data.bearer, + email_address, + created_at: Date.now(), + }), + ) }) } diff --git a/next.config.js b/next.config.js index 45f5fc866..1aaf117ce 100644 --- a/next.config.js +++ b/next.config.js @@ -17,9 +17,9 @@ const DEFAULT_LOCALE = 'en' function getSupportedLanguages() { const supportedLanguages = new Set() supportedLanguages.add(DEFAULT_LOCALE) // at least 1 supported language - glob.sync(`${LANG_DIR}/**/*.json`).forEach((f) => - supportedLanguages.add(basename(f, '.json')) - ) + glob + .sync(`${LANG_DIR}/**/*.json`) + .forEach((f) => supportedLanguages.add(basename(f, '.json'))) return [...supportedLanguages] } diff --git a/package.json b/package.json index f75b80648..0855ef1ff 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,6 @@ "@nivo/calendar": "^0.80.0", "@nivo/core": "^0.80.0", "@nivo/funnel": "^0.80.0", - "@nivo/heatmap": "^0.80.0", "@nivo/line": "^0.80.0", "@nivo/tooltip": "0.80.0", "@sentry/nextjs": "^7.109.0", @@ -30,12 +29,12 @@ "markdown-to-jsx": "^7.4.0", "next": "^14.2.4", "nprogress": "^0.2.0", - "ooni-components": "0.6.0-alpha.8", + "ooni-components": "0.7.0-alpha.1", "pretty-ms": "^8.0.0", "prop-types": "^15.8.1", "react": "^18.3.1", "react-content-loader": "^6.2.1", - "react-day-picker": "^8.10.0", + "react-day-picker": "^8.10.1", "react-dom": "^18.3.1", "react-hook-form": "^7.52.0", "react-icons": "^5.2.1", @@ -48,8 +47,8 @@ "react-virtualized": "^9.22.5", "react-window": "^1.8.10", "regenerator-runtime": "^0.14.1", - "styled-components": "^6.1.8", "swr": "^2.2.5", + "tailwind-merge": "^2.3.0", "use-clipboard-copy": "^0.2.0", "victory": "32.0.2", "yup": "^1.3.3" @@ -58,18 +57,21 @@ "jackspeak": "2.1.1" }, "devDependencies": { + "@biomejs/biome": "^1.8.3", "@next/bundle-analyzer": "^14.2.1", - "@biomejs/biome": "^1.7.0", "@svgr/webpack": "^8.1.0", "@testing-library/cypress": "^10.0.1", "@welldone-software/why-did-you-render": "^8.0.1", + "autoprefixer": "^10.4.19", "cypress": "^13.6.2", "glob": "^10.3.10", "imap-simple": "^5.1.0", "jsdom": "^23.2.0", "msw": "^2.1.0", "mustache": "^4.2.0", - "start-server-and-test": "^2.0.3" + "postcss": "^8.4.38", + "start-server-and-test": "^2.0.3", + "tailwindcss": "^3.4.4" }, "scripts": { "dev": "next dev -p 3100", diff --git a/pages/404.js b/pages/404.js index 0c79f391f..e8f74f8b3 100644 --- a/pages/404.js +++ b/pages/404.js @@ -1,14 +1,6 @@ import Head from 'next/head' import Link from 'next/link' import { useRouter } from 'next/router' -import { - Box, - Container, - Flex, - Heading, - Text -} from 'ooni-components' -import React from 'react' import { FormattedMessage, useIntl } from 'react-intl' import OONI404 from '../public/static/images/OONI_404.svg' @@ -19,43 +11,53 @@ const Custom404 = () => { return ( <> - {intl.formatMessage({id: 'Error.404.PageNotFound'})} + {intl.formatMessage({ id: 'Error.404.PageNotFound' })} - - - - - - - +
+
+
+

+ +

+
- {message => {message}} - , - homePageLink: - {message => {message}} - + measurementLink: ( + + {(message) => {message}} + + ), + homePageLink: ( + + {(message) => {message}} + + ), }} /> - - - - {message => - {e.preventDefault(); router.back()}}> +
+ +
+
+ +
+
+
) } diff --git a/pages/_app.js b/pages/_app.js index 385c38355..01bc5f0c5 100644 --- a/pages/_app.js +++ b/pages/_app.js @@ -1,4 +1,4 @@ -//FROM: +//FROM: // https://github.com/zeit/next.js/blob/master/examples/with-sentry // https://github.com/vercel/next.js/blob/canary/examples/with-loading/pages/_app.js import { Fira_Sans } from 'next/font/google' @@ -8,10 +8,13 @@ import { useEffect } from 'react' import 'scripts/wdyr' import dynamic from 'next/dynamic' -import '../public/static/nprogress.css' +import 'public/static/nprogress.css' +import 'styles/globals.css' const Layout = dynamic(() => import('components/Layout')) -const LocaleProvider = dynamic(() => import('components/withIntl').then((c) => c.LocaleProvider)) +const LocaleProvider = dynamic(() => + import('components/withIntl').then((c) => c.LocaleProvider), +) export const firaSans = Fira_Sans({ weight: ['300', '400', '600'], @@ -23,7 +26,6 @@ export default function App({ Component, pageProps, err }) { useEffect(() => { const handleStart = (url) => { - NProgress.start() } const handleStop = () => { diff --git a/pages/_document.js b/pages/_document.js index 825d182e3..76b5e7d95 100644 --- a/pages/_document.js +++ b/pages/_document.js @@ -1,37 +1,15 @@ -// updated based on documentation: https://github.com/vercel/next.js/blob/canary/examples/with-styled-components/pages/_document.tsx -import Document, { DocumentContext, Html, Head, Main, NextScript } from 'next/document' -import { ServerStyleSheet } from 'styled-components' import { getDirection } from 'components/withIntl' +import Document, { Head, Html, Main, NextScript } from 'next/document' export default class MyDocument extends Document { - static async getInitialProps(ctx) { - const sheet = new ServerStyleSheet() - const originalRenderPage = ctx.renderPage - - try { - ctx.renderPage = () => - originalRenderPage({ - enhanceApp: (App) => (props) => sheet.collectStyles(), - }) - - const initialProps = await Document.getInitialProps(ctx) - return { - ...initialProps, - styles: [initialProps.styles, sheet.getStyleElement()], - } - } finally { - sheet.seal() - } - } - render = () => ( - + />
diff --git a/pages/_error.js b/pages/_error.js index cc7598982..2fab55682 100644 --- a/pages/_error.js +++ b/pages/_error.js @@ -14,4 +14,4 @@ ErrorPage.getInitialProps = async (contextData) => { return NextErrorComponent.getInitialProps(contextData) } -export default ErrorPage \ No newline at end of file +export default ErrorPage diff --git a/pages/api/cloudflare.js b/pages/api/cloudflare.js index e608cef96..f57d8dc8d 100644 --- a/pages/api/cloudflare.js +++ b/pages/api/cloudflare.js @@ -8,11 +8,11 @@ const CACHE_MAX_AGE_IN_S = process.env.CACHE_MAX_AGE_IN_S || 60 * 60 const context = { cache: new LRUCache({ max: CACHE_MAX_SIZE, - ttl: CACHE_MAX_AGE_IN_S - }) + ttl: CACHE_MAX_AGE_IN_S, + }), } -const cache = handler => (req, res) => { +const cache = (handler) => (req, res) => { req.cache = context.cache return handler(req, res) } @@ -24,9 +24,15 @@ const cloudflareHandler = (req, res) => { const asn = req.query?.asn if (country && asn) { - return res.status(400).json('Country and asn can only be requested individually.') + return res + .status(400) + .json('Country and asn can only be requested individually.') } - if (!dayjs(dateStart).isValid() || dayjs(dateStart).isAfter(dayjs()) || dayjs(dateStart).isAfter(dayjs(dateEnd))) { + if ( + !dayjs(dateStart).isValid() || + dayjs(dateStart).isAfter(dayjs()) || + dayjs(dateStart).isAfter(dayjs(dateEnd)) + ) { return res.status(400).json('Invalid Start Date') } if (!dayjs(dateEnd).isValid() || dayjs(dateEnd).isAfter(dayjs())) { @@ -45,40 +51,43 @@ const cloudflareHandler = (req, res) => { const diff = dayjs(dateEnd).diff(dayjs(dateStart), 'day') const aggInterval = diff <= 30 ? '1h' : '1d' const targetParam = asn ? `asn=${asn}` : `location=${country}` - const formattedFrom = dateStart.split('.')[0]+'Z' - const formattedTo = dateEnd.split('.')[0]+'Z' + const formattedFrom = dateStart.split('.')[0] + 'Z' + const formattedTo = dateEnd.split('.')[0] + 'Z' return axios({ - method:'get', - url:`https://api.cloudflare.com/client/v4/radar/netflows/timeseries?name=all&product=all&dateStart=${formattedFrom}&dateEnd=${formattedTo}&${targetParam}&aggInterval=${aggInterval}&normalization=MIN0_MAX`, + method: 'get', + url: `https://api.cloudflare.com/client/v4/radar/netflows/timeseries?name=all&product=all&dateStart=${formattedFrom}&dateEnd=${formattedTo}&${targetParam}&aggInterval=${aggInterval}&normalization=MIN0_MAX`, headers: { - Authorization: `Bearer ${process.env.CLOUDFLARE_TOKEN}` + Authorization: `Bearer ${process.env.CLOUDFLARE_TOKEN}`, }, - }).then(({data, headers}) => { - const timestamps = data.result.all.timestamps - const values = data.result.all.values - const chartData = timestamps.map((st, i) => { - return { - 'x': st, - 'y': Number(values[i]) - } - }) - - if (req.cache) { - req.cache.set(cacheKey, { - headers, - data: chartData + }) + .then(({ data, headers }) => { + const timestamps = data.result.all.timestamps + const values = data.result.all.values + const chartData = timestamps.map((st, i) => { + return { + x: st, + y: Number(values[i]), + } }) - } - return res.status(200).json(chartData) - }).catch((err) =>{ - const responseError = err?.response?.data?.errors[0]?.message || err.message - return res.status(400).json(responseError) - }) + if (req.cache) { + req.cache.set(cacheKey, { + headers, + data: chartData, + }) + } + + return res.status(200).json(chartData) + }) + .catch((err) => { + const responseError = + err?.response?.data?.errors[0]?.message || err.message + return res.status(400).json(responseError) + }) } export default cache(cloudflareHandler) // Example of a query to request all and http traffic at the same time -// `https://api.cloudflare.com/client/v4/radar/netflows/timeseries?name=all&product=all&dateStart=${dateStart}&dateEnd=${dateEnd}&location=${location}&name=http&product=http&dateStart=${dateStart}&dateEnd=${dateEnd}&location=${location}&aggInterval=${aggInterval}&normalization=MIN0_MAX` \ No newline at end of file +// `https://api.cloudflare.com/client/v4/radar/netflows/timeseries?name=all&product=all&dateStart=${dateStart}&dateEnd=${dateEnd}&location=${location}&name=http&product=http&dateStart=${dateStart}&dateEnd=${dateEnd}&location=${location}&aggInterval=${aggInterval}&normalization=MIN0_MAX` diff --git a/pages/api/ioda.js b/pages/api/ioda.js index 45bf41b25..1cc879713 100644 --- a/pages/api/ioda.js +++ b/pages/api/ioda.js @@ -1,6 +1,5 @@ import axios from 'axios' import dayjs from 'services/dayjs' -import https from 'https' const iodaHandler = (req, res) => { const dateStart = req.query?.from @@ -9,9 +8,15 @@ const iodaHandler = (req, res) => { const asn = req.query?.asn if (country && asn) { - return res.status(400).json('Country and asn can only be requested individually.') + return res + .status(400) + .json('Country and asn can only be requested individually.') } - if (!dayjs(dateStart).isValid() || dayjs(dateStart).isAfter(dayjs()) || dayjs(dateStart).isAfter(dayjs(dateEnd))) { + if ( + !dayjs(dateStart).isValid() || + dayjs(dateStart).isAfter(dayjs()) || + dayjs(dateStart).isAfter(dayjs(dateEnd)) + ) { return res.status(400).json('Invalid Start Date') } if (!dayjs(dateEnd).isValid() || dayjs(dateEnd).isAfter(dayjs())) { @@ -21,30 +26,37 @@ const iodaHandler = (req, res) => { const diff = dayjs(dateEnd).diff(dayjs(dateStart), 'day') const aggInterval = diff <= 30 ? '1h' : '1d' const location = country || asn - const formattedFrom = Math.round(dayjs(dateStart).utc().valueOf()/1000) - const formattedTo = Math.round(dayjs(dateEnd).utc().valueOf()/1000) + const formattedFrom = Math.round(dayjs(dateStart).utc().valueOf() / 1000) + const formattedTo = Math.round(dayjs(dateEnd).utc().valueOf() / 1000) - const datasources = [ 'gtr', 'merit-nt', 'bgp', 'ping-slash24' ] + const datasources = ['gtr', 'merit-nt', 'bgp', 'ping-slash24'] return axios({ method: 'get', url: `http://api.ioda.inetintel.cc.gatech.edu/v2/signals/raw/${country ? 'country' : 'asn'}/${location}?from=${formattedFrom}&until=${formattedTo}&sourceParams=WEB_SEARCH`, - }).then(({data}) => { - const result = data.data[0] - .filter((item) => datasources.includes(item.datasource)) - .map((item, i) => { - const max = Math.max(...item.values) - const values = item.values.map((val, i) => { - const time = dayjs(item.from * 1000).add(item.step * i, 'second').utc().toISOString().split('.')[0]+'Z' - return {x: time, y: val ? val / max : null} - }) - return { datasource: item.datasource, values } - }) - - return res.status(200).json(result) - }).catch((err) =>{ - return res.status(400).json(err) }) + .then(({ data }) => { + const result = data.data[0] + .filter((item) => datasources.includes(item.datasource)) + .map((item, i) => { + const max = Math.max(...item.values) + const values = item.values.map((val, i) => { + const time = + dayjs(item.from * 1000) + .add(item.step * i, 'second') + .utc() + .toISOString() + .split('.')[0] + 'Z' + return { x: time, y: val ? val / max : null } + }) + return { datasource: item.datasource, values } + }) + + return res.status(200).json(result) + }) + .catch((err) => { + return res.status(400).json(err) + }) } -export default iodaHandler \ No newline at end of file +export default iodaHandler diff --git a/pages/as/[probe_asn].js b/pages/as/[probe_asn].js index 2a9637e44..49b19b5f4 100644 --- a/pages/as/[probe_asn].js +++ b/pages/as/[probe_asn].js @@ -2,22 +2,20 @@ import axios from 'axios' import CallToActionBox from 'components/CallToActionBox' import Chart from 'components/Chart' import CountryList from 'components/CountryBox' -import { StyledHollowButton, StyledSticky } from 'components/SharedStyledComponents' +import { StyledSticky } from 'components/SharedStyledComponents' import ThirdPartyDataChart from 'components/ThirdPartyDataChart' import Calendar from 'components/as/Calendar' import Loader from 'components/as/Loader' import Form from 'components/domain/Form' import ResultsList from 'components/search/ResultsList' import Head from 'next/head' -import NLink from 'next/link' +import Link from 'next/link' import { useRouter } from 'next/router' -import { Box, Container, Flex, Heading, Text } from 'ooni-components' -import React, { useMemo } from 'react' +import { useMemo } from 'react' import { MdOutlineSearch } from 'react-icons/md' import { FormattedMessage, useIntl } from 'react-intl' import dayjs from 'services/dayjs' import { fetcherWithPreprocessing, simpleFetcher } from 'services/fetchers' -import styled from 'styled-components' import useSWR from 'swr' import { getLocalisedRegionName } from 'utils/i18nCountries' import { toCompactNumberUnit } from '../../utils' @@ -35,13 +33,12 @@ const prepareDataForCalendar = (data) => { })) } -const StyledSection = styled(Box)`` -StyledSection.defaultProps = { - as: 'section', - my: 4, -} - -const messagingTestNames = ['signal', 'telegram', 'whatsapp', 'facebook_messenger'] +const messagingTestNames = [ + 'signal', + 'telegram', + 'whatsapp', + 'facebook_messenger', +] const circumventionTestNames = ['psiphon', 'tor', 'torsf'] const ChartsContainer = () => { @@ -61,7 +58,7 @@ const ChartsContainer = () => { test_name: 'web_connectivity', time_grain: 'day', }), - [probe_asn, since, until, probe_cc] + [probe_asn, since, until, probe_cc], ) const queryMessagingApps = useMemo( @@ -75,7 +72,7 @@ const ChartsContainer = () => { ...(probe_cc && { probe_cc }), time_grain: 'day', }), - [probe_asn, since, until, probe_cc] + [probe_asn, since, until, probe_cc], ) const queryCircumventionTools = useMemo( @@ -89,23 +86,23 @@ const ChartsContainer = () => { ...(probe_cc && { probe_cc }), time_grain: 'day', }), - [probe_asn, since, until, probe_cc] + [probe_asn, since, until, probe_cc], ) return ( <> - - +
+ - - - +
+
+ - - - +
+
+ - +
) } @@ -115,32 +112,28 @@ export const RecentMeasurements = ({ recentMeasurements, query }) => { return ( <> - +

{intl.formatMessage({ id: 'Domain.RecentMeasurements.Title' })} - +

- - + + + ) } const StatsItem = ({ label, unit = null, value }) => ( - - {label} - +
+
{label}
+
{value} - {unit && ( - - {unit} - - )} - - + {unit && {unit}} +
+
) const Summary = ({ measurementsTotal, firstMeasurement, lastMeasurement }) => { @@ -149,7 +142,10 @@ const Summary = ({ measurementsTotal, firstMeasurement, lastMeasurement }) => { dateStyle: 'long', timeZone: 'UTC', }).format(new Date(firstMeasurement)) - measurementsTotal = measurementsTotal < 10000 ? { value: measurementsTotal } : toCompactNumberUnit(measurementsTotal) + measurementsTotal = + measurementsTotal < 10000 + ? { value: measurementsTotal } + : toCompactNumberUnit(measurementsTotal) const formattedLastMeasurement = new Intl.DateTimeFormat(intl.locale, { dateStyle: 'long', timeZone: 'UTC', @@ -157,10 +153,10 @@ const Summary = ({ measurementsTotal, firstMeasurement, lastMeasurement }) => { return ( <> - - - - +

+ +

+
{ label={intl.formatMessage({ id: 'Network.Summary.LastMeasurement' })} value={formattedLastMeasurement} /> - +
) } @@ -184,19 +180,25 @@ const Summary = ({ measurementsTotal, firstMeasurement, lastMeasurement }) => { const CountriesList = ({ countriesData }) => { const { locale } = useIntl() - const sortedCountries = useMemo(() => ( + const sortedCountries = useMemo(() => countriesData .sort((a, b) => b.measurements - a.measurements) - .map((c) => ({...c, localisedName: getLocalisedRegionName(c.alpha_2, locale)})) - )) - + .map((c) => ({ + ...c, + localisedName: getLocalisedRegionName(c.alpha_2, locale), + })), + ) + const numberOfCountries = countriesData.length return ( <> - - - +

+ +

) @@ -217,37 +219,43 @@ const NetworkDashboard = ({ probe_asn, networkName, countriesData }) => { until: query.until ?? today.format('YYYY-MM-DD'), } }, [query]) + console.log('since', since, until) const { data: calendarData, error: calendarDataError } = useSWR( [ '/api/v1/aggregation', - { params: { - probe_asn, - since: dayjs.utc().subtract(12, 'year').format('YYYY-MM-DD'), - until: dayjs.utc().add(1, 'day').format('YYYY-MM-DD'), - axis_x: 'measurement_start_day', + { + params: { + probe_asn, + since: dayjs.utc().subtract(12, 'year').format('YYYY-MM-DD'), + until: dayjs.utc().add(1, 'day').format('YYYY-MM-DD'), + axis_x: 'measurement_start_day', }, resultKey: 'result', preprocessFn: prepareDataForCalendar, }, ], fetcherWithPreprocessing, - swrOptions + swrOptions, ) + console.log(calendarDataError) + const measurementsTotal = useMemo(() => { - return calendarData?.length ? calendarData.reduce((a, b) => a + b.value, 0) : null + return calendarData?.length + ? calendarData.reduce((a, b) => a + b.value, 0) + : null }, [calendarData]) const { data: recentMeasurements, error: recentMeasurementsError } = useSWR( ['/api/v1/measurements', { limit: 5, failure: false, probe_asn }], simpleFetcher, - swrOptions + swrOptions, ) // Sync page URL params with changes from form values const onSubmit = (data) => { - let params = {} + const params = {} for (const p of Object.keys(data)) { if (data[p]) { params[p] = data[p] @@ -259,7 +267,11 @@ const NetworkDashboard = ({ probe_asn, networkName, countriesData }) => { const { since, until, probe_cc } = params - if (query.since !== since || query.until !== until || query.probe_cc !== probe_cc) { + if ( + query.since !== since || + query.until !== until || + query.probe_cc !== probe_cc + ) { router.push({ query: params }, undefined, { shallow: true }) } } @@ -268,13 +280,14 @@ const NetworkDashboard = ({ probe_asn, networkName, countriesData }) => { <> - {intl.formatMessage({ id: 'General.OoniExplorer' })} | {probe_asn} {networkName} + {intl.formatMessage({ id: 'General.OoniExplorer' })} | {probe_asn}{' '} + {networkName} - - +
+

{probe_asn} {networkName} - +

{router.isReady && ( <> {calendarData === undefined && } @@ -287,33 +300,45 @@ const NetworkDashboard = ({ probe_asn, networkName, countriesData }) => { lastMeasurement={calendarData[calendarData.length - 1].day} /> - +

{intl.formatMessage({ id: 'Network.Stats.Title' })} - - +

+
{/* we want sticky header only while scrolling over the charts */} - -
c.country)} /> -
+
+ c.country)} + /> +
- - {since && until && } +
+ {since && until && ( + + )} {!!recentMeasurements?.length && ( - + )} )} {calendarData?.length === 0 && ( } - text={} + title={} + text={} /> )} )} - +
) } @@ -336,7 +361,7 @@ export const getServerSideProps = async (context) => { response.data.result.map((res) => ({ alpha_2: res.probe_cc, count: res.measurement_count, - })) + })), ) const networkName = await client diff --git a/pages/chart/circumvention.js b/pages/chart/circumvention.js index 68e03b161..0e572caeb 100644 --- a/pages/chart/circumvention.js +++ b/pages/chart/circumvention.js @@ -1,8 +1,7 @@ import axios from 'axios' import dayjs from 'dayjs' import { useRouter } from 'next/router' -import { Box, Container, Heading } from 'ooni-components' -import React, { useCallback, useEffect } from 'react' +import { useCallback, useEffect } from 'react' import { FormattedMessage } from 'react-intl' import FormattedMarkdown from 'components/FormattedMarkdown' @@ -24,72 +23,81 @@ const DashboardCircumvention = ({ availableCountries }) => { since: monthAgo, until: tomorrow, probe_cc, - ...query + ...query, }, } router.replace(href, undefined, { shallow: true }) }, []) // Sync page URL params with changes from form values - const onChange = useCallback(({ since, until, probe_cc }) => { - // since: "2022-01-02", - // until: "2022-02-01", - // probe_cc: "IT,AL,IR" - const params = { - since, - until - } - if (probe_cc) { - params['probe_cc'] = probe_cc - } - if (query.since !== since - || query.until !== until - || query.probe_cc !== probe_cc - ) { - router.push({ query: params }, undefined, { shallow: true }) - } - }, [router, query]) + const onChange = useCallback( + ({ since, until, probe_cc }) => { + // since: "2022-01-02", + // until: "2022-02-01", + // probe_cc: "IT,AL,IR" + const params = { + since, + until, + } + if (probe_cc) { + params.probe_cc = probe_cc + } + if ( + query.since !== since || + query.until !== until || + query.probe_cc !== probe_cc + ) { + router.push({ query: params }, undefined, { shallow: true }) + } + }, + [router, query], + ) return ( <> - - - - - - {Object.keys(query).length > 0 && - <> - - - - } - +
+

+ +

+
+ +
+ {Object.keys(query).length > 0 && ( + <> + + + + )} +
) } // Fetch list of countries for which we have data for circumvention tools // Used to populate the country selection list in the form -export async function getServerSideProps () { +export async function getServerSideProps() { let availableCountries = [] try { - const client = axios.create({baseURL: process.env.NEXT_PUBLIC_OONI_API}) // eslint-disable-line + const client = axios.create({ baseURL: process.env.NEXT_PUBLIC_OONI_API }) // eslint-disable-line const res = await client.get('/api/_/circumvention_stats_by_country') const { results } = res.data - availableCountries = results.map(d => d.probe_cc) - + availableCountries = results.map((d) => d.probe_cc) } catch (e) { console.error(e) // Sentry.captureException(e) } finally { return { props: { - availableCountries - } + availableCountries, + }, } } } -export default DashboardCircumvention \ No newline at end of file +export default DashboardCircumvention diff --git a/pages/chart/mat.js b/pages/chart/mat.js index 79220114c..d92d1c29b 100644 --- a/pages/chart/mat.js +++ b/pages/chart/mat.js @@ -1,8 +1,6 @@ -/* global process */ import Head from 'next/head' import { useRouter } from 'next/router' -import { Box, Container, Flex, Heading, Link } from 'ooni-components' -import React, { useCallback, useEffect } from 'react' +import { useCallback, useEffect } from 'react' import { FormattedMessage, useIntl } from 'react-intl' import MATChart from 'components/MATChart' @@ -18,7 +16,7 @@ const MeasurementAggregationToolkit = () => { const onSubmit = useCallback( (data) => { - let params = {} + const params = {} for (const p of Object.keys(data)) { if (data[p] !== '') { params[p] = data[p] @@ -30,7 +28,7 @@ const MeasurementAggregationToolkit = () => { } return router.push(href, href, { shallow: true }) }, - [router] + [router], ) // Upon mount, check if the page was accessed without query params @@ -59,7 +57,7 @@ const MeasurementAggregationToolkit = () => { let linkToAPIQuery = null try { linkToAPIQuery = `${process.env.NEXT_PUBLIC_OONI_API}/api/v1/aggregation?${new URLSearchParams( - query + query, ).toString()}` } catch (e) { console.error(`Failed to construct API query link: ${e.message}`) @@ -70,43 +68,45 @@ const MeasurementAggregationToolkit = () => { {intl.formatMessage({ id: 'MAT.Title' })} - - - +
+
+

- - +

+
- +
{linkToAPIQuery && ( - - - - - {intl.formatMessage({ id: 'MAT.JSONData' })} - - - - - - {intl.formatMessage({ id: 'MAT.CSVData' })} - - - - - + )} - +
- - - +
+
+
) } diff --git a/pages/countries.js b/pages/countries.js index 698ed4876..a5b10a17b 100644 --- a/pages/countries.js +++ b/pages/countries.js @@ -1,112 +1,88 @@ import axios from 'axios' +import CountryList from 'components/CountryBox' +import countryUtil from 'country-util' import debounce from 'lodash.debounce' import Head from 'next/head' -import { - Box, - Container, Flex, Heading, Input, Text -} from 'ooni-components' -import React, { useMemo, useState } from 'react' +import { Input } from 'ooni-components' +import { useMemo, useState } from 'react' import { FormattedMessage, useIntl } from 'react-intl' -import styled from 'styled-components' - -import CountryList from 'components/CountryBox' -import countryUtil from 'country-util' import { getLocalisedRegionName } from 'utils/i18nCountries' import { StickySubMenu } from '../components/SharedStyledComponents' -// To compenstate for the sticky navigation bar -// :target selector applies only the element with id that matches -// the current URL fragment (e.g '/#Africa') -const RegionHeaderAnchor = styled.div` - /* Height of the combined header (NavBar and Regions) */ - /* This is needed to compensate the for the sticky navbar and region - links bar when scrolling to the selected region. And the height of these - bars changes in the mobile layout. This has evolved to be a bad design - that needs to be replaced. */ - height: 140px; - margin-top: -140px; - @media(max-width: 768px) { - height: 200px; - margin-top: -200px; - } -` - -const Regions = ({ regions, countries}) => { - return ( - regions.map((regionCode, index) => { - - const measuredCountriesInRegion = countryUtil.regions[regionCode].countries.filter((countryCode) => ( - countries.find((item) => item.alpha_2 === countryCode) - )) - - return ( - ( measuredCountriesInRegion.indexOf(c.alpha_2) > -1 )))} - /> - ) - }) - ) +const Regions = ({ regions, countries }) => { + return regions.map((regionCode, index) => { + const measuredCountriesInRegion = countryUtil.regions[ + regionCode + ].countries.filter((countryCode) => + countries.find((item) => item.alpha_2 === countryCode), + ) + + return ( + measuredCountriesInRegion.indexOf(c.alpha_2) > -1, + )} + /> + ) + }) } -const RegionBlock = ({regionCode, countries}) => { +const RegionBlock = ({ regionCode, countries }) => { const intl = useIntl() - const regionName = useMemo(() => (getLocalisedRegionName(regionCode, intl.locale)), [regionCode, intl]) - + const regionName = useMemo( + () => getLocalisedRegionName(regionCode, intl.locale), + [regionCode, intl], + ) + // When there are no measurements from the region if (countries.length === 0) { return null } return ( - - - {regionName} - +
- +

{regionName}

+ +
) } -const StyledRegionLink = styled.a` - display: block; - color: ${(props) => props.theme.colors.blue5}; - text-decoration: underline; -` - const RegionLink = ({ href, label }) => ( - - - {label} - - + + {label} + ) const NoCountriesFound = ({ searchTerm }) => ( - - - - {/* TODO Add to copy */} +
+
+
- - - +
+
+
) export const getStaticProps = async () => { - const client = axios.create({baseURL: process.env.NEXT_PUBLIC_OONI_API}) // eslint-disable-line + const client = axios.create({ baseURL: process.env.NEXT_PUBLIC_OONI_API }) // eslint-disable-line const result = await client.get('/api/_/countries') return { props: { countries: result.data.countries, - } + }, } } @@ -114,20 +90,33 @@ const Countries = ({ countries }) => { const intl = useIntl() const [searchInput, setSearchInput] = useState('') - const sortedCountries = useMemo(() => (countries - .map((c) => ({...c, localisedName: getLocalisedRegionName(c.alpha_2, intl.locale)})) - .sort((a, b) => (new Intl.Collator(intl.locale).compare(a.localisedName, b.localisedName))) - ), - [intl, countries] + const sortedCountries = useMemo( + () => + countries + .map((c) => ({ + ...c, + localisedName: getLocalisedRegionName(c.alpha_2, intl.locale), + })) + .sort((a, b) => + new Intl.Collator(intl.locale).compare( + a.localisedName, + b.localisedName, + ), + ), + [intl, countries], ) - const filteredCountries = useMemo(() => ( - searchInput !== '' ? - sortedCountries.filter((country) => ( - country.name.toLowerCase().indexOf(searchInput.toLowerCase()) > -1 - )) : - sortedCountries - ), [searchInput]) + const filteredCountries = useMemo( + () => + searchInput !== '' + ? sortedCountries.filter( + (country) => + country.name.toLowerCase().indexOf(searchInput.toLowerCase()) > + -1, + ) + : sortedCountries, + [searchInput], + ) const searchHandler = (searchTerm) => { setSearchInput(searchTerm) @@ -141,43 +130,58 @@ const Countries = ({ countries }) => { return ( <> - {intl.formatMessage({id: 'Countries.PageTitle'})} + {intl.formatMessage({ id: 'Countries.PageTitle' })} - +
- - +
+
debouncedSearchHandler(e.target.value)} - placeholder={intl.formatMessage({id: 'Countries.Search.Placeholder'})} + placeholder={intl.formatMessage({ + id: 'Countries.Search.Placeholder', + })} error={filteredCountries.length === 0} /> - - - - - - - - - - +
+
+ + + + + + +
+
- + { // Show a message when there are no countries to show, when search is empty - (filteredCountries.length === 0) - ? - : + filteredCountries.length === 0 ? ( + + ) : ( + + ) } - +
) } diff --git a/pages/country/[countryCode].js b/pages/country/[countryCode].js index 47f5e1476..8eab14a0b 100644 --- a/pages/country/[countryCode].js +++ b/pages/country/[countryCode].js @@ -3,15 +3,17 @@ import CountryDetails from 'components/country/CountryDetails' import ErrorPage from 'pages/_error' const getCountryReports = (countryCode, data) => { - const reports = data.filter((article) => ( - article.tags && article.tags.indexOf(`country-${countryCode.toLowerCase()}`) > -1 - )).map((article) => ( - article - )) + const reports = data + .filter( + (article) => + article.tags && + article.tags.indexOf(`country-${countryCode.toLowerCase()}`) > -1, + ) + .map((article) => article) return reports } -export async function getServerSideProps ({ res, query }) { +export async function getServerSideProps({ res, query }) { const { countryCode } = query if (countryCode.length > 2) { return { @@ -22,21 +24,23 @@ export async function getServerSideProps ({ res, query }) { } } - if (res && (countryCode !== countryCode.toUpperCase())) { + if (res && countryCode !== countryCode.toUpperCase()) { res.writeHead(301, { - Location: `/country/${countryCode.toUpperCase()}` + Location: `/country/${countryCode.toUpperCase()}`, }) res.end() } try { - let client = axios.create({baseURL: process.env.NEXT_PUBLIC_OONI_API}) // eslint-disable-line - let results = await Promise.all([ + const client = axios.create({ baseURL: process.env.NEXT_PUBLIC_OONI_API }) // eslint-disable-line + const results = await Promise.all([ // XXX cc @darkk we should ideally have better dedicated daily dumps for this view - client.get('/api/_/test_coverage', {params: {'probe_cc': countryCode}}), - client.get('/api/_/country_overview', { params: {'probe_cc': countryCode}}), - client.get('https://ooni.org/pageindex.json') + client.get('/api/_/test_coverage', { params: { probe_cc: countryCode } }), + client.get('/api/_/country_overview', { + params: { probe_cc: countryCode }, + }), + client.get('https://ooni.org/pageindex.json'), ]) const testCoverage = results[0].data.test_coverage const networkCoverage = results[0].data.network_coverage @@ -50,29 +54,36 @@ export async function getServerSideProps ({ res, query }) { overviewStats, reports, countryCode, - } + }, } } catch (error) { return { props: { - error: JSON.stringify(error?.message) - } + error: JSON.stringify(error?.message), + }, } } } -const Country = ({ countryCode, overviewStats, reports, error, ...coverageDataSSR }) => { +const Country = ({ + countryCode, + overviewStats, + reports, + error, + ...coverageDataSSR +}) => { return ( <> - {!!error ? - : + {error ? ( + + ) : ( - } + )} ) } diff --git a/pages/domain/[domain].js b/pages/domain/[domain].js index c515c41e0..006366c9b 100644 --- a/pages/domain/[domain].js +++ b/pages/domain/[domain].js @@ -1,61 +1,47 @@ import axios from 'axios' -import Flag from 'components/Flag' -import { useRouter } from 'next/router' -import { Box, Container, Flex, Heading, Text } from 'ooni-components' -import { useMemo, useState } from 'react' -import { useIntl } from 'react-intl' - import TestGroupBadge, { CategoryBadge } from 'components/Badge' import Chart from 'components/DomainChart' +import Flag from 'components/Flag' import { StyledSticky } from 'components/SharedStyledComponents' import { GridBox } from 'components/VirtualizedGrid' import Form from 'components/domain/Form' import Head from 'next/head' -import NLink from 'next/link' +import Link from 'next/link' +import { useRouter } from 'next/router' import { RecentMeasurements } from 'pages/as/[probe_asn]' +import { useMemo, useState } from 'react' +import { useIntl } from 'react-intl' import { simpleFetcher } from 'services/fetchers' import useSWR from 'swr' import { getLocalisedRegionName } from 'utils/i18nCountries' import BlockText from '../../components/BlockText' import { sortByKey } from '../../utils' -const CountryList = ({ countries, itemsPerRow = 6, gridGap = 3 }) => { +const CountryList = ({ countries }) => { const intl = useIntl() - const gridTemplateColumns = [ - '1fr 1fr', - '1fr 1fr', - '1fr 1fr 1fr 1fr', - [...Array(itemsPerRow)].map((i) => '1fr').join(' '), - ] return ( - +
{countries.map(([key, value]) => { return ( - +
+
- - +
+
{getLocalisedRegionName(key, intl.locale)} - - +
+
} multiCount={value} /> ) })} -
+
) } @@ -80,12 +66,12 @@ const ChartContainer = ({ domain, ...props }) => { until, time_grain: 'day', }), - [domain, since, until] + [domain, since, until], ) return ( <> - + ) @@ -99,19 +85,32 @@ const BlockingCountries = ({ blockedCountries }) => { return ( <> - - {intl.formatMessage({ id: 'Domain.CountriesBlocking.Title' }, { domain })} - - - {intl.formatMessage({ id: 'Domain.CountriesBlocking.FromTo' }, { since, until })} - - {!!blockedCountries?.length ? ( +

+ {intl.formatMessage( + { id: 'Domain.CountriesBlocking.Title' }, + { domain }, + )} +

+
+ {intl.formatMessage( + { id: 'Domain.CountriesBlocking.FromTo' }, + { since, until }, + )} +
+ {blockedCountries?.length ? ( <> - {intl.formatMessage({ id: 'Domain.CountriesBlocking.Subtitle' })} + + {intl.formatMessage({ id: 'Domain.CountriesBlocking.Subtitle' })} + ) : ( - {intl.formatMessage({ id: 'Domain.CountriesBlocking.NoCountries' }, { domain })} + <> + {intl.formatMessage( + { id: 'Domain.CountriesBlocking.NoCountries' }, + { domain }, + )} + )} ) @@ -120,22 +119,25 @@ const BlockingCountries = ({ blockedCountries }) => { const Canonical = ({ canonicalDomain }) => { const intl = useIntl() return ( - + {intl.formatMessage( { id: 'Domain.Canonical' }, { canonicalDomain: ( - - {canonicalDomain} - + {canonicalDomain} ), - } + }, )} ) } -const DomainDashboard = ({ domain, categoryCode, canonicalDomain, countries }) => { +const DomainDashboard = ({ + domain, + categoryCode, + canonicalDomain, + countries, +}) => { const router = useRouter() const { query } = router const [testedCountries, setTestedCountries] = useState(null) @@ -150,7 +152,8 @@ const DomainDashboard = ({ domain, categoryCode, canonicalDomain, countries }) = const countryData = accum.get(current.probe_cc) accum.set(current.probe_cc, { - confirmed_count: countryData.confirmed_count + current.confirmed_count, + confirmed_count: + countryData.confirmed_count + current.confirmed_count, anomaly_count: countryData.anomaly_count + current.anomaly_count, ok_count: countryData.ok_count + current.ok_count, }) @@ -173,11 +176,11 @@ const DomainDashboard = ({ domain, categoryCode, canonicalDomain, countries }) = const { data: recentMeasurements, error: recentMeasurementsError } = useSWR( ['/api/v1/measurements', { limit: 5, failure: false, domain }], simpleFetcher, - swrOptions + swrOptions, ) const onSubmit = (data) => { - let params = {} + const params = {} for (const p of Object.keys(data)) { if (data[p]) { params[p] = data[p] @@ -187,7 +190,11 @@ const DomainDashboard = ({ domain, categoryCode, canonicalDomain, countries }) = const { since, until, probe_cc } = params - if (query.since !== since || query.until !== until || query.probe_cc !== probe_cc) { + if ( + query.since !== since || + query.until !== until || + query.probe_cc !== probe_cc + ) { router.push({ query: params }, undefined, { shallow: true }) } } @@ -199,28 +206,36 @@ const DomainDashboard = ({ domain, categoryCode, canonicalDomain, countries }) = {title} - - - {domain} - - +
+

{domain}

+
- +
{hasCanonical && } - +
{/* we want sticky header only while scrolling over the charts */} - - c.alpha_2)} /> - +
+ c.alpha_2)} + /> +
- +
- - - {blockedCountries && } - {!!recentMeasurements?.length && } - +
+
+ {blockedCountries && ( + + )} + {!!recentMeasurements?.length && ( + + )} +
) } @@ -232,9 +247,10 @@ export const getServerSideProps = async (context) => { const client = axios.create({ baseURL: process.env.NEXT_PUBLIC_OONI_API }) const path = '/api/_/domain_metadata' - const { canonical_domain: canonicalDomain, category_code: categoryCode } = await client - .get(path, { params: { domain } }) - .then((response) => response.data) + const { canonical_domain: canonicalDomain, category_code: categoryCode } = + await client + .get(path, { params: { domain } }) + .then((response) => response.data) const countriesR = await client.get('/api/_/countries') const countries = countriesR.data.countries diff --git a/pages/domains.js b/pages/domains.js index 28f0bbe10..d4e0e8b4b 100644 --- a/pages/domains.js +++ b/pages/domains.js @@ -4,7 +4,7 @@ import { StickySubMenu } from 'components/SharedStyledComponents' import { getCategoryCodesMap } from 'components/utils/categoryCodes' import useFilterWithSort from 'hooks/useFilterWithSort' import Head from 'next/head' -import { Box, Container, Flex, Input, Select, Text } from 'ooni-components' +import { Input, Select } from 'ooni-components' import { useMemo } from 'react' import { useIntl } from 'react-intl' import { simpleFetcher } from 'services/fetchers' @@ -15,15 +15,15 @@ const categoryCodes = [...getCategoryCodesMap().keys()] const sortOptions = [ { key: 'domain_asc', intlKey: 'Sort.DomainAsc' }, - { key: 'domain_desc', intlKey:'Sort.DomainDesc' }, + { key: 'domain_desc', intlKey: 'Sort.DomainDesc' }, { key: 'measurements_asc', intlKey: 'Sort.MeasurementCountAsc' }, { key: 'measurements_desc', intlKey: 'Sort.MeasurementCountDesc' }, { key: 'category_asc', intlKey: 'Sort.CategoryNameAsc' }, - { key: 'category_desc', intlKey: 'Sort.CategoryNameDesc' } + { key: 'category_desc', intlKey: 'Sort.CategoryNameDesc' }, ] const Domains = () => { - const {data, error} = useSWR('/api/_/domains', simpleFetcher) + const { data, error } = useSWR('/api/_/domains', simpleFetcher) const intl = useIntl() const { @@ -32,56 +32,72 @@ const Domains = () => { setSortValue, categoryValue, setCategoryValue, - debouncedSearchHandler - } = useFilterWithSort({initialSortValue: 'measurements_desc'}) + debouncedSearchHandler, + } = useFilterWithSort({ initialSortValue: 'measurements_desc' }) const displayData = useMemo(() => { if (data) { return data - .filter((value, index, self) => // first remove duplicates - index === self.findIndex((d) => ( - d.domain_name === value.domain_name - )) + .filter( + ( + value, + index, + self, // first remove duplicates + ) => + index === + self.findIndex((d) => d.domain_name === value.domain_name), ) .map((domain) => ({ - ...domain, - title: {domain.domain_name}, - id: domain.domain_name, - tag: , - count: domain.measurement_count, - href: `/domain/${domain.domain_name}`, - }) - ) - } else { - return [] + ...domain, + title:
{domain.domain_name}
, + id: domain.domain_name, + tag: , + count: domain.measurement_count, + href: `/domain/${domain.domain_name}`, + })) } + return [] }, [data]) const sortedAndFilteredData = useMemo(() => { const sortedData = displayData.sort((a, b) => { if (sortValue === 'measurements_asc') { return a.count - b.count - } if (sortValue === 'measurements_desc') { + } + if (sortValue === 'measurements_desc') { return b.count - a.count - } else if (sortValue === 'domain_desc') { - return b.domain_name.localeCompare(a.domain_name, intl.locale, { numeric: true }) - } else if (sortValue === 'category_asc') { + } + if (sortValue === 'domain_desc') { + return b.domain_name.localeCompare(a.domain_name, intl.locale, { + numeric: true, + }) + } + if (sortValue === 'category_asc') { return a.category_code.localeCompare(b.category_code, intl.locale) - } else if (sortValue === 'category_desc') { + } + if (sortValue === 'category_desc') { return b.category_code.localeCompare(a.category_code, intl.locale) - } else { - // default to 'domain_asc' sort - return a.domain_name.localeCompare(b.domain_name, intl.locale, { numeric: true }) } + // default to 'domain_asc' sort + return a.domain_name.localeCompare(b.domain_name, intl.locale, { + numeric: true, + }) }) - const filteredData = !!searchValue.length || !!categoryValue.length ? - sortedData.filter((domain) => { - const fitsSearchValue = !!searchValue ? domain.domain_name.toLowerCase().includes(searchValue.toLowerCase()) : true - const fitsCategoryCode = !!categoryValue ? categoryValue === domain.category_code : true - return fitsSearchValue && fitsCategoryCode - }) : - sortedData + const filteredData = + !!searchValue.length || !!categoryValue.length + ? sortedData.filter((domain) => { + const fitsSearchValue = searchValue + ? domain.domain_name + .toLowerCase() + .includes(searchValue.toLowerCase()) + : true + const fitsCategoryCode = categoryValue + ? categoryValue === domain.category_code + : true + return fitsSearchValue && fitsCategoryCode + }) + : sortedData return filteredData }, [displayData, sortValue, searchValue, categoryValue, intl.locale]) @@ -89,59 +105,70 @@ const Domains = () => { return ( <> - {intl.formatMessage({id: 'General.OoniExplorer'})} | {intl.formatMessage({id: 'Domains.Title'})} + + {intl.formatMessage({ id: 'General.OoniExplorer' })} |{' '} + {intl.formatMessage({ id: 'Domains.Title' })} + - +
- { intl.formatMessage({id: 'Domains.Title'})}{' '}{!!sortedAndFilteredData.length && - <>({new Intl.NumberFormat().format(sortedAndFilteredData.length)}) - } + {intl.formatMessage({ id: 'Domains.Title' })}{' '} + {!!sortedAndFilteredData.length && ( + <> + ( + {new Intl.NumberFormat().format(sortedAndFilteredData.length)} + ) + + )} } > - - - debouncedSearchHandler(e.target.value)} - placeholder={intl.formatMessage({id: 'Domains.SearchPlaceholder'})} - error={ - (searchValue && sortedAndFilteredData?.length === 0) && - <>{intl.formatMessage({id: 'Domains.SearchError'})} - } - /> - - - - - - - - +
+ debouncedSearchHandler(e.target.value)} + placeholder={intl.formatMessage({ + id: 'Domains.SearchPlaceholder', + })} + error={ + searchValue && + sortedAndFilteredData?.length === 0 && + intl.formatMessage({ id: 'Domains.SearchError' }) + } + /> + + +
- - {!!displayData.length ? - : - - } - - +
+ {displayData.length ? ( + + ) : ( + + )} +
+
) } -export default Domains \ No newline at end of file +export default Domains diff --git a/pages/findings/[id].js b/pages/findings/[id].js index 5b1d98458..43d388495 100644 --- a/pages/findings/[id].js +++ b/pages/findings/[id].js @@ -1,5 +1,4 @@ import Head from 'next/head' -import { Container } from 'ooni-components' import { apiEndpoints, fetcher } from '/lib/api' import NotFound from 'components/NotFound' @@ -8,22 +7,29 @@ import { useMemo } from 'react' import { useIntl } from 'react-intl' export const getServerSideProps = async ({ query }) => { - const data = await fetcher(apiEndpoints.SHOW_INCIDENT.replace(':id', query.id)).catch(() => (null)) + const data = await fetcher( + apiEndpoints.SHOW_INCIDENT.replace(':id', query.id), + ).catch(() => null) return { props: { data, - } + }, } } const ReportView = ({ data }) => { const intl = useIntl() - const metaTitle = useMemo(() => ( - `${intl.formatMessage({ id: 'General.OoniExplorer' })}${!!data?.incident?.title && ` - ${data?.incident?.title}`}` - ), [data, intl]) - const metaDescription = useMemo(() => (data?.incident?.short_description || ''), [data]) + const metaTitle = useMemo( + () => + `${intl.formatMessage({ id: 'General.OoniExplorer' })}${!!data?.incident?.title && ` - ${data?.incident?.title}`}`, + [data, intl], + ) + const metaDescription = useMemo( + () => data?.incident?.short_description || '', + [data], + ) return ( <> @@ -31,16 +37,27 @@ const ReportView = ({ data }) => { {metaTitle} - + - + - - {data ? - : - - } - +
+ {data ? ( + + ) : ( + + )} +
) } diff --git a/pages/findings/create.js b/pages/findings/create.js index fb7da7f50..ae81192ce 100644 --- a/pages/findings/create.js +++ b/pages/findings/create.js @@ -1,8 +1,6 @@ import { createIncidentReport, getUserEmail } from 'lib/api' -import Head from 'next/head' -import NLink from 'next/link' +import Link from 'next/link' import { useRouter } from 'next/router' -import { Button, Container, Flex, Heading } from 'ooni-components' import { useEffect, useState } from 'react' import { useIntl } from 'react-intl' import dayjs from 'services/dayjs' @@ -45,25 +43,34 @@ const Create = () => { }, []) const onSubmit = (report) => { - return createIncidentReport(report).then((data) => router.push(`/findings/${data.id}`)) + return createIncidentReport(report).then((data) => + router.push(`/findings/${data.id}`), + ) } return ( <> - + {/* - - {user ? - + */} + {user ? ( +
- - {intl.formatMessage({id: 'Findings.Create.Title'})} - - +
+

{intl.formatMessage({ id: 'Findings.Create.Title' })}

+ + + +
- : - - } +
+ ) : ( +
+ +
+ )} ) } diff --git a/pages/findings/dashboard.js b/pages/findings/dashboard.js index 89ff0c3e6..cbee879f2 100644 --- a/pages/findings/dashboard.js +++ b/pages/findings/dashboard.js @@ -6,29 +6,23 @@ import { } from '@tanstack/react-table' import SpinLoader from 'components/vendor/SpinLoader' import useUser from 'hooks/useUser' -import { apiEndpoints, fetcher, publishIncidentReport, unpublishIncidentReport } from 'lib/api' -import Head from 'next/head' -import NLink from 'next/link' +import { + apiEndpoints, + fetcher, + publishIncidentReport, + unpublishIncidentReport, +} from 'lib/api' +import Link from 'next/link' import { useRouter } from 'next/router' -import { Button, Container, Flex, Heading } from 'ooni-components' import { useEffect, useMemo, useState } from 'react' import { FaSort, FaSortDown, FaSortUp } from 'react-icons/fa' import { useIntl } from 'react-intl' import { ToastContainer, toast } from 'react-toastify' import 'react-toastify/dist/ReactToastify.css' -import { styled } from 'styled-components' import useSWR from 'swr' import useSWRMutation from 'swr/mutation' import { formatMediumDate } from 'utils' -const StyledTable = styled.table` -border-collapse: collapse; -width: 100%; -th, td { - border: 1px solid gray; -} -` - const Dashboard = () => { const intl = useIntl() const { user, loading } = useUser() @@ -36,34 +30,40 @@ const Dashboard = () => { const { data, error, mutate } = useSWR(apiEndpoints.SEARCH_INCIDENTS, fetcher) - const tableData = useMemo(() => (data?.incidents ? data.incidents : []), [data]) + const tableData = useMemo( + () => (data?.incidents ? data.incidents : []), + [data], + ) const [sorting, setSorting] = useState([]) const onError = (error) => { toast.error(`Error: ${error?.message}`, { position: toast.POSITION.BOTTOM_RIGHT, - toastId: 'error' + toastId: 'error', }) } const { trigger: publish, isMutating: isPublishMutating } = useSWRMutation( 'publish', (_, { arg }) => publishIncidentReport(arg), - { onSuccess: () => { mutate() }, + { + onSuccess: () => { + mutate() + }, throwOnError: false, - onError - } + onError, + }, ) - const { trigger: unpublish, isMutating: isUnpublishMutating } = useSWRMutation( - 'unpublish', - (_, { arg }) => unpublishIncidentReport(arg), - { onSuccess: () => { mutate() }, + const { trigger: unpublish, isMutating: isUnpublishMutating } = + useSWRMutation('unpublish', (_, { arg }) => unpublishIncidentReport(arg), { + onSuccess: () => { + mutate() + }, throwOnError: false, - onError - } - ) + onError, + }) // redirect non-admin users useEffect(() => { @@ -75,14 +75,16 @@ const Dashboard = () => { { header: 'Title', accessorKey: 'title', - cell: info => ( - {info.getValue()} - ) + cell: (info) => ( + + {info.getValue()} + + ), }, { header: 'Last Update', accessorKey: 'update_time', - cell: (info) => (formatMediumDate(info.getValue())) + cell: (info) => formatMediumDate(info.getValue()), }, { header: 'Reported by', @@ -95,41 +97,55 @@ const Dashboard = () => { { header: 'Start Date', accessorKey: 'start_time', - cell: (info) => (formatMediumDate(info.getValue())) + cell: (info) => formatMediumDate(info.getValue()), }, { header: 'End Date', accessorKey: 'end_time', - cell: (info) => (info.getValue() && formatMediumDate(info.getValue())) + cell: (info) => info.getValue() && formatMediumDate(info.getValue()), }, { header: 'Published', accessorKey: 'published', - cell: (info) => (info.getValue() ? '✅' : '❌') + cell: (info) => (info.getValue() ? '✅' : '❌'), }, { header: '', accessorKey: 'id', - cell: info => ( + cell: (info) => ( <> - - - - {info.row.original.published ? - : - - } + + + + {info.row.original.published ? ( + + ) : ( + + )} - ) - } + ), + }, ], - [] + [], ) const table = useReactTable({ @@ -145,20 +161,31 @@ const Dashboard = () => { return ( <> - + {/* - + */} {user?.role === 'admin' ? ( - +
- {intl.formatMessage({id: 'Findings.Dashboard.Title'})} - +

+ {intl.formatMessage({ id: 'Findings.Dashboard.Title' })} +

+ + {/* +th, td { + border: 1px solid gray; +} */} +
- {table.getHeaderGroups().map(headerGroup => ( + {table.getHeaderGroups().map((headerGroup) => ( {headerGroup.headers.map((header, i) => { return ( - @@ -186,38 +228,41 @@ const Dashboard = () => { ))} - {table - .getRowModel() - .rows - .map(row => { - return ( - - {row.getVisibleCells().map(cell => { - return ( - - ) - })} - - ) - })} + {table.getRowModel().rows.map((row) => { + return ( + + {row.getVisibleCells().map((cell) => { + return ( + + ) + })} + + ) + })} - - - - - - - - - - +
+ {header.isPlaceholder ? null : (
{ : '', onClick: header.column.getToggleSortingHandler(), }} - style={{display: 'ruby', cursor: 'pointer'}} + style={{ display: 'ruby', cursor: 'pointer' }} > {flexRender( header.column.columnDef.header, - header.getContext() + header.getContext(), )} {{ - asc: <> , - desc: <> , - }[header.column.getIsSorted()] ?? <> } + asc: ( + <> + {' '} + + + ), + desc: ( + <> + {' '} + + + ), + }[header.column.getIsSorted()] ?? ( + <> + {' '} + + + )}
)}
- {flexRender( - cell.column.columnDef.cell, - cell.getContext() - )} -
+ {flexRender( + cell.column.columnDef.cell, + cell.getContext(), + )} +
+
+ + + + + + +
+
) : ( - +
+ +
)} ) diff --git a/pages/findings/edit/[id].js b/pages/findings/edit/[id].js index f4d1c612b..8d8cfa258 100644 --- a/pages/findings/edit/[id].js +++ b/pages/findings/edit/[id].js @@ -1,9 +1,8 @@ import Form from 'components/findings/Form' import useUser from 'hooks/useUser' import Head from 'next/head' -import NLink from 'next/link' +import Link from 'next/link' import { useRouter } from 'next/router' -import { Button, Container, Flex, Heading } from 'ooni-components' import { useEffect, useMemo } from 'react' import { useIntl } from 'react-intl' import useSWR from 'swr' @@ -23,13 +22,14 @@ const EditReport = () => { query.id && user ? apiEndpoints.SHOW_INCIDENT.replace(':id', query.id) : null, - fetcher + fetcher, ) // redirect if user not logged in or not admin/report creator useEffect(() => { if (!user && !loading) router.replace('/findings') - if ((data && !data.incident.mine) && user?.role !== 'admin') router.replace('/findings') + if (data && !data.incident.mine && user?.role !== 'admin') + router.replace('/findings') }, [user, loading, router, data]) const defaultValues = useMemo(() => { @@ -38,50 +38,56 @@ const EditReport = () => { rest.start_time = rest.start_time.split('T')[0] rest.end_time = rest?.end_time ? rest.end_time.split('T')[0] : null return rest - } else { - return null } + return null }, [data]) const onSubmit = (report) => { - return updateIncidentReport(report).then((data) => router.push(`/findings/${data.id}`)) + return updateIncidentReport(report).then((data) => + router.push(`/findings/${data.id}`), + ) } const { trigger, isMutating } = useSWRMutation( `DELETE${query.id}`, - () => deleteIncidentReport({id: query.id}), + () => deleteIncidentReport({ id: query.id }), { onSuccess: () => { router.push('/findings/dashboard') }, - } + }, ) return ( <> - + {/* - - - - {intl.formatMessage({id: 'Findings.Edit.Title'})} - - + */} +
+
+

{intl.formatMessage({ id: 'Findings.Edit.Title' })}

+ + + +
{defaultValues && ( <> - + > + {intl.formatMessage({ id: 'Findings.Edit.Delete' })} + )} - +
) } diff --git a/pages/findings/index.js b/pages/findings/index.js index dbffc39c8..892c9cbc0 100644 --- a/pages/findings/index.js +++ b/pages/findings/index.js @@ -3,45 +3,32 @@ import HighlightBox from 'components/landing/HighlightBox' import SpinLoader from 'components/vendor/SpinLoader' import useFilterWithSort from 'hooks/useFilterWithSort' import useUser from 'hooks/useUser' -import Head from 'next/head' -import NLink from 'next/link' -import { Box, Button, Container, Flex, Heading, Input, Select, Text } from 'ooni-components' +import Link from 'next/link' +import { Input, Select } from 'ooni-components' import { useMemo } from 'react' import { useIntl } from 'react-intl' -import { styled } from 'styled-components' import useSWR from 'swr' import { formatLongDate } from 'utils' import { apiEndpoints, fetcher } from '/lib/api' const sortOptions = [ { key: 'start_asc', intlKey: 'Sort.StartAsc' }, - { key: 'start_desc', intlKey:'Sort.StartDesc' }, + { key: 'start_desc', intlKey: 'Sort.StartDesc' }, { key: 'end_asc', intlKey: 'Sort.EndAsc' }, { key: 'end_desc', intlKey: 'Sort.EndDesc' }, ] -const StyledGrid = styled(Box)` -display: grid; -grid-template-columns: 1fr 1fr; -gap: 24px; - -@media screen and (max-width: 48em) { - grid-template-columns: 1fr; -} -` - const Index = () => { const intl = useIntl() const { user } = useUser() - const { data, isLoading, error } = useSWR(apiEndpoints.SEARCH_INCIDENTS, fetcher) + const { data, isLoading, error } = useSWR( + apiEndpoints.SEARCH_INCIDENTS, + fetcher, + ) - const { - searchValue, - sortValue, - setSortValue, - debouncedSearchHandler - } = useFilterWithSort({initialSortValue: 'end_desc'}) + const { searchValue, sortValue, setSortValue, debouncedSearchHandler } = + useFilterWithSort({ initialSortValue: 'end_desc' }) const displayData = useMemo(() => { if (data) { @@ -50,105 +37,154 @@ const Index = () => { .map((incident) => ({ ...incident, start_time: new Date(incident.start_time), - ...(incident.end_time && {end_time: new Date(incident.end_time)}), - sort_end_time: incident.end_time ? new Date(incident.end_time) : new Date() + ...(incident.end_time && { end_time: new Date(incident.end_time) }), + sort_end_time: incident.end_time + ? new Date(incident.end_time) + : new Date(), })) - } else { - return [] } + return [] }, [data]) const sortedAndFilteredData = useMemo(() => { const sortedData = displayData - .sort((a, b) => (b.start_time - a.start_time)) // make sure ongoing events are always chronologically sorted + .sort((a, b) => b.start_time - a.start_time) // make sure ongoing events are always chronologically sorted .sort((a, b) => { if (sortValue === 'start_asc') { return a.start_time - b.start_time - } else if (sortValue === 'start_desc') { + } + if (sortValue === 'start_desc') { return b.start_time - a.start_time - } else if (sortValue === 'end_asc') { + } + if (sortValue === 'end_asc') { return a.sort_end_time - b.sort_end_time - } else { - // default to 'end_desc' sort - return b.sort_end_time - a.sort_end_time - }}) + } + // default to 'end_desc' sort + return b.sort_end_time - a.sort_end_time + }) - const filteredData = !!searchValue.length ? - sortedData.filter((incident) => ( - !!searchValue ? - incident.title.toLowerCase().includes(searchValue.toLowerCase()) || incident.short_description.toLowerCase().includes(searchValue.toLowerCase()) : - true - )) : - sortedData + const filteredData = searchValue.length + ? sortedData.filter((incident) => + searchValue + ? incident.title + .toLowerCase() + .includes(searchValue.toLowerCase()) || + incident.short_description + .toLowerCase() + .includes(searchValue.toLowerCase()) + : true, + ) + : sortedData return filteredData }, [displayData, sortValue, searchValue]) return ( <> - + {/* - - + */} +
{user?.role === 'admin' && ( - +
+ + + +
)} {intl.formatMessage({id: 'Findings.Index.Title'}, {amount: sortedAndFilteredData.length})}} + title={ + <> + {intl.formatMessage( + { id: 'Findings.Index.Title' }, + { amount: sortedAndFilteredData.length }, + )} + + } > - - - debouncedSearchHandler(e.target.value)} - placeholder={intl.formatMessage({id: 'Findings.Index.SearchPlaceholder'})} - error={ - (searchValue && sortedAndFilteredData?.length === 0) && - <>{intl.formatMessage({id: 'Findings.Index.SearchError'})} - } - /> - - - - - +
+ debouncedSearchHandler(e.target.value)} + placeholder={intl.formatMessage({ + id: 'Findings.Index.SearchPlaceholder', + })} + error={ + searchValue && + sortedAndFilteredData?.length === 0 && ( + <> + {intl.formatMessage({ id: 'Findings.Index.SearchError' })} + + ) + } + /> + +
- {isLoading && - - } - {!!sortedAndFilteredData?.length && - + {isLoading && ( +
+ +
+ )} + {!!sortedAndFilteredData?.length && ( +
{sortedAndFilteredData.map((incident) => ( - - - {incident.start_time && formatLongDate(incident.start_time, intl.locale)} - {incident.end_time ? formatLongDate(incident.end_time, intl.locale) : 'ongoing'} - - - {intl.formatMessage({id: 'Findings.Index.HighLightBox.CreatedOn'}, {date: incident?.create_time && formatLongDate(incident?.create_time, intl.locale)})} - - +
+
+ {incident.start_time && + formatLongDate(incident.start_time, intl.locale)}{' '} + -{' '} + {incident.end_time + ? formatLongDate(incident.end_time, intl.locale) + : 'ongoing'} +
+
+ {intl.formatMessage( + { id: 'Findings.Index.HighLightBox.CreatedOn' }, + { + date: + incident?.create_time && + formatLongDate(incident?.create_time, intl.locale), + }, + )} +
+
} footer={ - - - - - +
+ + + +
} /> ))} - - } - +
+ )} +
) } diff --git a/pages/index.js b/pages/index.js index 22de19055..d2dccafbc 100644 --- a/pages/index.js +++ b/pages/index.js @@ -1,149 +1,73 @@ /* global process */ import axios from 'axios' import Head from 'next/head' -import NLink from 'next/link' -import Router from 'next/router' -import { - Box, - Button, - Container, - Flex, - Heading, - Link, - Text -} from 'ooni-components' +import Link from 'next/link' +import { colors } from 'ooni-components' import PropTypes from 'prop-types' -import React from 'react' import { FormattedMessage, useIntl } from 'react-intl' -import styled from 'styled-components' +import { twMerge } from 'tailwind-merge' import FormattedMarkdown from '../components/FormattedMarkdown' - import HighlightSection from '../components/landing/HighlightsSection' import CoverageChart from '../components/landing/Stats' import highlightContent from '../components/landing/highlights.json' import { toCompactNumberUnit } from '../utils' -const HeroUnit = styled.div` - background: linear-gradient( - 319.33deg, - ${props => props.theme.colors.blue9} 39.35%, - ${props => props.theme.colors.base} 82.69%), - ${props => props.theme.colors.base}; - background-size: contain; - background-repeat: no-repeat; - background-position: center; - padding-bottom: 48px; - padding-top: 16px; -` - -const ExploreButton = styled(Button)` - font-weight: normal; - color: white; - border: 2px solid white; - border-radius: 48px; - height: 60px; - cursor: pointer; /* until added to Button in ooni-components */ - - &:hover, &:focus { - background-color: white; - color: ${props => props.theme.colors.blue5}; - } - - &:active { - background-color: white; - color: ${props => props.theme.colors.blue5}; - box-shadow: 0 0 0 0.2rem ${props => props.theme.colors.blue4}; - } -` - -const StatsContainer = styled(Flex)` - border-radius: 16px; - background-color: #ffffff; -` - -const StyledLabel = styled.div` - color: ${props => props.theme.colors.gray7}; -` - -const StyledStatsItem = styled(Box)` - text-align: center; -` - -const StatsItem = ({label, unit, value }) => ( - - +const StatsItem = ({ label, unit, value }) => ( +
+
{value} - {unit} - - - {label} - - + + {unit} + +
+
{label}
+
) StatsItem.propTypes = { - label: PropTypes.oneOfType([ - PropTypes.element, - PropTypes.string - ]), + label: PropTypes.oneOfType([PropTypes.element, PropTypes.string]), unit: PropTypes.string, - value: PropTypes.number + value: PropTypes.number, } -const FeatureRow = styled(Flex)` - -` - -FeatureRow.defaultProps = { - flexWrap: 'wrap', - alignItems: 'center', - justifyContent: 'center', - py: 4 -} - -const FeatureBox = styled(Box)` - font-size: 20px; - line-height: 1.5; -` - -FeatureBox.defaultProps = { - width: [1, 1/2], - lineHeight: 1.5, -} - -const FeatureBoxTitle = styled(Flex)` -` -FeatureBoxTitle.defaultProps = { - color: 'blue9', - fontSize: 24, - fontWeight: 600, - mb: 2 -} +const FeatureRow = ({ className, ...props }) => ( +
+) -const ImgBox = styled.img` - width: 100%; -` +const FeatureBox = ({ className, ...props }) => ( +
+) -const StyledContainer = styled(Container)` - background-image: url('/static/images/world-dots.svg'); - background-repeat: no-repeat; - background-position: center; -` +const FeatureBoxTitle = ({ className, ...props }) => ( +
+) export async function getServerSideProps({ query }) { - const client = axios.create({baseURL: process.env.NEXT_PUBLIC_OONI_API}) // eslint-disable-line + const client = axios.create({ baseURL: process.env.NEXT_PUBLIC_OONI_API }) // eslint-disable-line const result = await client.get('/api/_/global_overview') return { props: { measurementCount: result.data.measurement_count, asnCount: result.data.network_count, - countryCount: result.data.country_count - } + countryCount: result.data.country_count, + }, } } -const LandingPage = ({ measurementCount, asnCount, countryCount}) => { +const LandingPage = ({ measurementCount, asnCount, countryCount }) => { const intl = useIntl() measurementCount = toCompactNumberUnit(measurementCount) asnCount = toCompactNumberUnit(asnCount) @@ -151,152 +75,175 @@ const LandingPage = ({ measurementCount, asnCount, countryCount}) => { return ( <> - {intl.formatMessage({id: 'General.OoniExplorer'})} + {intl.formatMessage({ id: 'General.OoniExplorer' })} - - - - - - - - - - - - - - - - - +
+
+
+

+ +

+
+ +
+ + + +
+
+
+
+
} + label={} unit={measurementCount.unit} value={measurementCount.value} /> } + label={} value={countryCount} /> } + label={} unit={asnCount.unit} value={asnCount.value} /> - +
{/* Intro text about Explorer */} - - - - - - - +
+
+ +
+
{/* Websites & Apps */} - + Websites and Apps - + - + - + {/* Search & Filter */} {/* Arrange in {[img, para], [img, para], [img, para]} pattern on smaller screens */} - - + + - + - + - + Search and Filter {/* Network Properties */} - + Network Properties - + - + - + {/* Measurement Statistics */} - - - +
+
+

- - +

+
- +
{/* Highlights */} - - - - +
+
+

+ - - - - - - - - - +

+
+
+
+ +
+
{/* Political Events */} } - description={} + title={} + description={ + + } highlights={highlightContent.political} /> {/* Media */} } - description={} + title={} + description={ + + } highlights={highlightContent.media} /> {/* LGBTQI sites */} } - description={} + title={} + description={ + + } highlights={highlightContent.lgbtqi} /> {/* Censorship changes */} } - description={} + title={} + description={ + + } highlights={highlightContent.changes} /> - - - ( - {string} - ) - }} - /> - - - - +
+ ( + {string} + ), + }} + /> +
+
+
) } @@ -304,7 +251,7 @@ const LandingPage = ({ measurementCount, asnCount, countryCount}) => { LandingPage.propTypes = { countryCount: PropTypes.number, asnCount: PropTypes.number, - measurementCount: PropTypes.number + measurementCount: PropTypes.number, } -export default LandingPage \ No newline at end of file +export default LandingPage diff --git a/pages/login.js b/pages/login.js index 4535c1def..fec51753a 100644 --- a/pages/login.js +++ b/pages/login.js @@ -1,12 +1,10 @@ -import Head from 'next/head' -import NLink from 'next/link' -import { useRouter } from 'next/router' -import { Box, Container, Flex, Heading, Text } from 'ooni-components' -import React, { useEffect, useState } from 'react' - import LoginForm from 'components/login/LoginForm' import SpinLoader from 'components/vendor/SpinLoader' import useUser from 'hooks/useUser' +import Head from 'next/head' +import Link from 'next/link' +import { useRouter } from 'next/router' +import { useEffect, useState } from 'react' import { FormattedMessage, useIntl } from 'react-intl' const Login = () => { @@ -30,60 +28,63 @@ const Login = () => { return ( <> - {intl.formatMessage({id: 'General.Login'})} + {intl.formatMessage({ id: 'General.Login' })} - - - +
+
+

- - - +

+
+
{/* Before logging In */} - {!token && !submitted && + {!token && !submitted && ( <> - +
- - - setSubmitted(true)} redirectTo={redirectTo} /> - +
+
+ setSubmitted(true)} + redirectTo={redirectTo} + /> +
- } - {!token && submitted && - + )} + {!token && submitted && ( +

- - } +

+ )} {/* While logging In */} - {token && !user && !error && + {token && !user && !error && ( <> - +

- +

- } + )} {/* After loggin in */} - {user && !error && token && - <> - - - - - } + {user && !error && token && ( +
+ +
+ )} {/* Errors */} - {error && - - {error} - - - } - - + {error && ( +
+
{error}
+ + + +
+ )} +
+
) } diff --git a/pages/m/[measurement_uid].js b/pages/m/[measurement_uid].js index 5f7eafe2c..daa74ccb2 100644 --- a/pages/m/[measurement_uid].js +++ b/pages/m/[measurement_uid].js @@ -1,8 +1,8 @@ import axios from 'axios' import Head from 'next/head' -import { Container, theme } from 'ooni-components' +import { colors } from 'ooni-components' import PropTypes from 'prop-types' -import React, { useMemo, useState } from 'react' +import { useMemo, useState } from 'react' import useSWR from 'swr' import { getLocalisedRegionName } from '/utils/i18nCountries' @@ -22,18 +22,18 @@ import NotFound from '../../components/NotFound' import { fetcher } from '/lib/api' const pageColors = { - default: theme.colors.base, - anomaly: theme.colors.yellow9, - reachable: theme.colors.green8, - error: theme.colors.gray6, - down: theme.colors.gray6, - confirmed: theme.colors.red7 + default: colors.blue['500'], + anomaly: colors.yellow['900'], + reachable: colors.green['800'], + error: colors.gray['600'], + down: colors.gray['600'], + confirmed: colors.red['700'], } export async function getServerSideProps({ query }) { let initialProps = { error: null, - userFeedback: null + userFeedback: null, } const measurement_uid = query?.measurement_uid @@ -41,46 +41,55 @@ export async function getServerSideProps({ query }) { if (typeof measurement_uid !== 'string' || measurement_uid.length < 10) { initialProps.notFound = true return { - props: initialProps + props: initialProps, } } let response let client try { - client = axios.create({baseURL: process.env.NEXT_PUBLIC_OONI_API}) // eslint-disable-line - let params = { + client = axios.create({ baseURL: process.env.NEXT_PUBLIC_OONI_API }) // eslint-disable-line + const params = { measurement_uid, - full: true + full: true, } if (query.input) { - params['input'] = query.input + params.input = query.input } try { response = await client.get('/api/v1/measurement_meta', { - params + params, }) } catch (e) { - throw new Error(`Failed to fetch measurement data. Server message: ${e.response.status}, ${e.response.statusText}`) + throw new Error( + `Failed to fetch measurement data. Server message: ${e.response.status}, ${e.response.statusText}`, + ) } // If response `data` is an empty object, the measurement was // probably not found - if (Object.prototype.hasOwnProperty.call(response, 'data') && Object.keys(response.data).length !== 0) { - initialProps = {...initialProps, ...response.data} + if ( + Object.prototype.hasOwnProperty.call(response, 'data') && + Object.keys(response.data).length !== 0 + ) { + initialProps = { ...initialProps, ...response.data } - if (typeof initialProps['scores'] === 'string') { + if (typeof initialProps.scores === 'string') { try { - initialProps['scores'] = JSON.parse(initialProps['scores']) + initialProps.scores = JSON.parse(initialProps.scores) } catch (e) { throw new Error(`Failed to parse JSON in scores: ${e.toString()}`) } } - initialProps['raw_measurement'] ? - initialProps['raw_measurement'] = JSON.parse(initialProps['raw_measurement']) : - initialProps.notFound = true + initialProps.raw_measurement + ? // biome-ignore lint/suspicious/noAssignInExpressions: + (initialProps.raw_measurement = JSON.parse( + initialProps.raw_measurement, + )) + : // biome-ignore lint/suspicious/noAssignInExpressions: + (initialProps.notFound = true) } else { // Measurement not found initialProps.notFound = true @@ -89,7 +98,7 @@ export async function getServerSideProps({ query }) { initialProps.error = e.message } return { - props: initialProps + props: initialProps, } } @@ -116,34 +125,39 @@ const Measurement = ({ const { user, setSubmitted } = useUser() const [showModal, setShowModal] = useState(false) - const {data: userFeedback, error: userFeedbackError, mutate: mutateUserFeedback} = useSWR(`/api/_/measurement_feedback/${measurement_uid}`, fetcher) + const { + data: userFeedback, + error: userFeedbackError, + mutate: mutateUserFeedback, + } = useSWR(`/api/_/measurement_feedback/${measurement_uid}`, fetcher) const userFeedbackItems = useMemo(() => { - return userFeedback ? - Object.entries(userFeedback.summary).map(([key, value]) => ( - {label: intl.formatMessage({id: `Measurement.Feedback.${key}`}), value} - )) : - [] + return userFeedback + ? Object.entries(userFeedback.summary).map(([key, value]) => ({ + label: intl.formatMessage({ id: `Measurement.Feedback.${key}` }), + value, + })) + : [] }, [userFeedback]) // Add the 'AS' prefix to probe_asn when API chooses to send just the number probe_asn = typeof probe_asn === 'number' ? `AS${probe_asn}` : probe_asn if (error) { - return ( - - ) + return } return ( <> - {intl.formatMessage({id: 'General.OoniExplorer'})} + {intl.formatMessage({ id: 'General.OoniExplorer' })} {notFound ? ( <> - + - ): ( + ) : ( <> { - const color = failure === true ? pageColors['error'] : pageColors[status] + const color = + failure === true ? pageColors.error : pageColors[status] const info = scores?.msg ?? statusInfo return ( <> - {headMetadata && + {headMetadata && ( - } - {showModal && + )} + {showModal && ( - } + )} } + hero={ + + } onVerifyClick={() => setShowModal(true)} /> - +
- {summaryText && + {summaryText && ( - } + )} {details} - +
) - } - } /> + }} + /> )} @@ -248,7 +269,7 @@ Measurement.propTypes = { report_id: PropTypes.string, measurement_uid: PropTypes.string, test_name: PropTypes.string, - measurement_start_time: PropTypes.string + measurement_start_time: PropTypes.string, } export default Measurement diff --git a/pages/measurement/[[...report_id]].js b/pages/measurement/[[...report_id]].js index 493654bef..8d7740370 100644 --- a/pages/measurement/[[...report_id]].js +++ b/pages/measurement/[[...report_id]].js @@ -7,25 +7,28 @@ export async function getServerSideProps({ query }) { let error = null // Get `report_id` using optional catch all dynamic route of Next.js - // Doc: https://nextjs.org/docs/routing/dynamic-routes#optional-catch-all-routes + // Doc: https://nextjs.org/docs/routing/dynamic-routes#optional-catch-all-routes // e.g /measurement/20211015T162758Z_webconnectivity_TH_23969_n1_d11S0T15FaOuXgFO // It can also catch /measurement/report_id/extra/segments // in which case, the extra segments are available inside query.report_id[1+] const report_id = query?.report_id?.[0] // If there is no report_id to use, fail early with NotFound - if (typeof report_id !== 'string' || !report_id.match(/[a-zA-Z0-9_-]{40,100}/)) { + if ( + typeof report_id !== 'string' || + !report_id.match(/[a-zA-Z0-9_-]{40,100}/) + ) { return { - props: {} + props: {}, } } - const client = axios.create({baseURL: process.env.NEXT_PUBLIC_OONI_API}) // eslint-disable-line + const client = axios.create({ baseURL: process.env.NEXT_PUBLIC_OONI_API }) // eslint-disable-line const params = { report_id, - full: true + full: true, } if (query.input) { - params['input'] = query.input + params.input = query.input } let response @@ -44,10 +47,10 @@ export async function getServerSideProps({ query }) { } } - return { + return { props: { - ...(error && error) - } + ...(error && error), + }, } } @@ -56,10 +59,11 @@ const Measurement = ({ error }) => { return ( <> - {error ? - : - - } + {error ? ( + + ) : ( + + )} ) } diff --git a/pages/networks.js b/pages/networks.js index 226e75049..d8bdb41e3 100644 --- a/pages/networks.js +++ b/pages/networks.js @@ -5,65 +5,65 @@ import { StickySubMenu } from 'components/SharedStyledComponents' import VirtualizedGrid from 'components/VirtualizedGrid' import useFilterWithSort from 'hooks/useFilterWithSort' import Head from 'next/head' -import { Box, Container, Flex, Heading, Input, Select } from 'ooni-components' +import { Input, Select } from 'ooni-components' import { useIntl } from 'react-intl' import { simpleFetcher } from 'services/fetchers' import useSWR from 'swr' const sortOptions = [ { key: 'asn_asc', intlKey: 'Sort.AsnAsc' }, - { key: 'asn_desc', intlKey:'Sort.AsnDesc' }, + { key: 'asn_desc', intlKey: 'Sort.AsnDesc' }, { key: 'measurements_asc', intlKey: 'Sort.MeasurementCountAsc' }, - { key: 'measurements_desc', intlKey: 'Sort.MeasurementCountDesc' } + { key: 'measurements_desc', intlKey: 'Sort.MeasurementCountDesc' }, ] const Networks = () => { const intl = useIntl() - const {data, error} = useSWR('/api/_/networks', simpleFetcher) + const { data, error } = useSWR('/api/_/networks', simpleFetcher) - const { - searchValue, - sortValue, - setSortValue, - debouncedSearchHandler - } = useFilterWithSort({initialSortValue: 'measurements_desc'}) + const { searchValue, sortValue, setSortValue, debouncedSearchHandler } = + useFilterWithSort({ initialSortValue: 'measurements_desc' }) const displayData = useMemo(() => { if (data) { - return data - .map((asn) => ({ - ...asn, - title: <>{`AS${asn.probe_asn}`}{asn.org_name}, - id: `AS${asn.probe_asn}`, - count: asn.cnt, - href: `/as/AS${asn.probe_asn}`, - } - )) - } else { - return [] + return data.map((asn) => ({ + ...asn, + title: ( + <> +
{`AS${asn.probe_asn}`}
+
{asn.org_name}
+ + ), + id: `AS${asn.probe_asn}`, + count: asn.cnt, + href: `/as/AS${asn.probe_asn}`, + })) } + return [] }, [data]) const sortedAndFilteredData = useMemo(() => { const sortedData = displayData.sort((a, b) => { if (sortValue === 'asn_desc') { return b.probe_asn - a.probe_asn - } else if (sortValue === 'measurements_asc') { + } + if (sortValue === 'measurements_asc') { return a.count - b.count - } else if (sortValue === 'measurements_desc') { + } + if (sortValue === 'measurements_desc') { return b.count - a.count - } else { - // use 'asn_asc' as default - return a.probe_asn - b.probe_asn } + // use 'asn_asc' as default + return a.probe_asn - b.probe_asn }) - const filteredData = searchValue.length ? - sortedData.filter((asn) => ( - asn.probe_asn.toString().includes(searchValue.toLowerCase()) || - asn.org_name.toLowerCase().includes(searchValue.toLowerCase()) - )) : - sortedData + const filteredData = searchValue.length + ? sortedData.filter( + (asn) => + asn.probe_asn.toString().includes(searchValue.toLowerCase()) || + asn.org_name.toLowerCase().includes(searchValue.toLowerCase()), + ) + : sortedData return filteredData }, [displayData, sortValue, searchValue]) @@ -71,45 +71,61 @@ const Networks = () => { return ( <> - {intl.formatMessage({id: 'General.OoniExplorer'})} | {intl.formatMessage({id: 'Networks.Title'})} + + {intl.formatMessage({ id: 'General.OoniExplorer' })} |{' '} + {intl.formatMessage({ id: 'Networks.Title' })} + - - - { - intl.formatMessage({id: 'Networks.Title'}) - } {!!sortedAndFilteredData.length && - <>({new Intl.NumberFormat().format(sortedAndFilteredData.length)}) - } - } +
+ + {intl.formatMessage({ id: 'Networks.Title' })}{' '} + {!!sortedAndFilteredData.length && ( + <> + ( + {new Intl.NumberFormat().format(sortedAndFilteredData.length)} + ) + + )} + + } > - - - debouncedSearchHandler(e.target.value)} - placeholder={intl.formatMessage({id: 'Networks.SearchPlaceholder'})} - error={ - (searchValue && !sortedAndFilteredData.length) && - <>{intl.formatMessage({id: 'Networks.SearchError'})} - } - /> - - - - - +
+ debouncedSearchHandler(e.target.value)} + placeholder={intl.formatMessage({ + id: 'Networks.SearchPlaceholder', + })} + error={ + searchValue && + !sortedAndFilteredData.length && ( + <>{intl.formatMessage({ id: 'Networks.SearchError' })} + ) + } + /> + +
- - {!!displayData.length ? - : - - } - - +
+ {displayData.length ? ( + + ) : ( +
+ +
+ )} +
+
) } diff --git a/pages/search.js b/pages/search.js index ea7488f38..2747f43ae 100644 --- a/pages/search.js +++ b/pages/search.js @@ -1,34 +1,31 @@ import axios from 'axios' import Head from 'next/head' import { useRouter } from 'next/router' -import { - Box, - Button, - Container, - Flex, - Heading, - Text -} from 'ooni-components' import PropTypes from 'prop-types' -import React, { useEffect, useState } from 'react' +import { useEffect, useState } from 'react' import { FormattedMessage, useIntl } from 'react-intl' import dayjs from 'services/dayjs' -import styled from 'styled-components' import dynamic from 'next/dynamic' import { sortByKey } from '../utils' import FormattedMarkdown from '/components/FormattedMarkdown' -import FilterSidebar, { queryToFilterMap } from '/components/search/FilterSidebar' +import FilterSidebar, { + queryToFilterMap, +} from '/components/search/FilterSidebar' import ResultsList from '/components/search/ResultsList' -const Loader = dynamic(() => import('/components/search/Loader'), { ssr: false }) +const Loader = dynamic(() => import('/components/search/Loader'), { + ssr: false, +}) -export const getServerSideProps = async ({query}) => { +export const getServerSideProps = async ({ query }) => { // By default, on '/search' show measurements published until today // including the measurements of today (so the date of tomorrow). // This prevents the search page from showing time-travelling future // measurements from showing up - query.since = query.since || dayjs(query.until).utc().subtract(30, 'day').format('YYYY-MM-DD') + query.since = + query.since || + dayjs(query.until).utc().subtract(30, 'day').format('YYYY-MM-DD') query.until = query.until || dayjs.utc().add(1, 'day').format('YYYY-MM-DD') // If there is no 'failure' in query, default to a false @@ -39,7 +36,7 @@ export const getServerSideProps = async ({query}) => { query.failure = !(query.failure === 'false') } - const client = axios.create({baseURL: process.env.NEXT_PUBLIC_OONI_API}) + const client = axios.create({ baseURL: process.env.NEXT_PUBLIC_OONI_API }) const countriesR = await client.get('/api/_/countries') const countries = countriesR.data.countries @@ -49,44 +46,54 @@ export const getServerSideProps = async ({query}) => { props: { countries, query, - } + }, } } const queryToParams = ({ query }) => { - let params = {}, - show = 50 - const supportedParams = ['probe_cc', 'domain', 'input','category_code', 'probe_asn', 'test_name', 'since', 'until', 'failure'] + const params = {} + let show = 50 + const supportedParams = [ + 'probe_cc', + 'domain', + 'input', + 'category_code', + 'probe_asn', + 'test_name', + 'since', + 'until', + 'failure', + ] if (query.show) { - show = parseInt(query.show) + show = Number.parseInt(query.show) } - params['limit'] = show + params.limit = show // Allow only `failure=false`. `true` results in showing only failures - if ('failure' in query && query['failure'] === false) { - params['failure'] = false + if ('failure' in query && query.failure === false) { + params.failure = false } for (const p of supportedParams) { - if (p in query && query[p] !== queryToFilterMap[p][1]) { + if (p in query && query[p] !== queryToFilterMap[p][1]) { params[p] = query[p] } } if (query.only) { if (query.only === 'anomalies') { - params['anomaly'] = true + params.anomaly = true } else if (query.only === 'confirmed') { - params['confirmed'] = true + params.confirmed = true } } return params } const getMeasurements = (query) => { - let client = axios.create({baseURL: process.env.NEXT_PUBLIC_OONI_API}) // eslint-disable-line + const client = axios.create({ baseURL: process.env.NEXT_PUBLIC_OONI_API }) // eslint-disable-line const params = queryToParams({ query }) - return client.get('/api/v1/measurements', {params}) + return client.get('/api/v1/measurements', { params }) } const serializeError = (err) => { @@ -96,64 +103,58 @@ const serializeError = (err) => { return { name, message, - data, status, statusText, - baseURL, url, params, - stack + data, + status, + statusText, + baseURL, + url, + params, + stack, } } -const StyledPre = styled.pre` - white-space: pre-wrap; /* css-3 */ - white-space: -moz-pre-wrap; /* Mozilla, since 1999 */ - white-space: -pre-wrap; /* Opera 4-6 */ - white-space: -o-pre-wrap; /* Opera 7 */ - word-wrap: break-word; /* Internet Explorer 5.5+ */ -` - const ErrorBox = ({ error }) => { if (!error) { - return
+ return
} const { stack, ...restOfError } = error return ( - - - - - - - - - - +
+
+
+ +
+
+ +
+
+
             {JSON.stringify(restOfError, null, '  ')}
-          
-        
-        
-          
-            {stack}
-          
-        
-      
-    
+          
+
+
+
{stack}
+
+
+
) } ErrorBox.propTypes = { - error: PropTypes.object.isRequired + error: PropTypes.object.isRequired, } const NoResults = () => ( - - - - - - - - +
+

+ +

+
+ +
+
) const Search = ({ countries, query: queryProp }) => { @@ -170,16 +171,23 @@ const Search = ({ countries, query: queryProp }) => { const q = query || queryProp const href = { pathname: '/search', - query: q + query: q, } replace(href, href, { shallow: true }) getMeasurements(q) - .then(({ data: { results, metadata: { next_url } } }) => { - setLoading(false) - setResults(results) - setNextURL(next_url) - }) + .then( + ({ + data: { + results, + metadata: { next_url }, + }, + }) => { + setLoading(false) + setResults(results) + setNextURL(next_url) + }, + ) .catch((err) => { console.error(err) const error = serializeError(err) @@ -189,11 +197,19 @@ const Search = ({ countries, query: queryProp }) => { }, []) const loadMore = () => { - axios.get(nextURL) - .then(({ data: { results: nextPageResults, metadata: { next_url } } }) => { - setResults(results.concat(nextPageResults)) - setNextURL(next_url) - }) + axios + .get(nextURL) + .then( + ({ + data: { + results: nextPageResults, + metadata: { next_url }, + }, + }) => { + setResults(results.concat(nextPageResults)) + setNextURL(next_url) + }, + ) .catch((err) => { console.error(err) const error = serializeError(err) @@ -210,26 +226,33 @@ const Search = ({ countries, query: queryProp }) => { const query = getFilterQuery(state) const href = { pathname: '/search', - query + query, } router.push(href, href, { shallow: true }).then(() => { getMeasurements(query) - .then(({ data: { results, metadata: { next_url } } }) => { - setLoading(false) - setResults(results) - setNextURL(next_url) - }) + .then( + ({ + data: { + results, + metadata: { next_url }, + }, + }) => { + setLoading(false) + setResults(results) + setNextURL(next_url) + }, + ) .catch((err) => { console.error(err) const error = serializeError(err) setError(error) setLoading(false) }) - }) + }) } const getFilterQuery = (state) => { - let query = {...router.query} + const query = { ...router.query } const resetValues = [undefined, 'XX', ''] for (const [queryParam, [key]] of Object.entries(queryToFilterMap)) { // If it's unset or marked as XX, let's be sure the path is clean @@ -237,7 +260,7 @@ const Search = ({ countries, query: queryProp }) => { if (queryParam in query) { delete query[queryParam] } - } else if (key === 'onlyFilter' && state[key] == 'all') { + } else if (key === 'onlyFilter' && state[key] === 'all') { // If the onlyFilter is not set to 'confirmed' or 'anomalies' // remove it from the path if (queryParam in query) { @@ -260,11 +283,11 @@ const Search = ({ countries, query: queryProp }) => { return ( <> - {intl.formatMessage({id: 'Search.PageTitle'})} + {intl.formatMessage({ id: 'Search.PageTitle' })} - - - +
+
+
{ onApplyFilter={onApplyFilter} countries={countries} /> - - +
+
{error && } - {loading && } + {loading && ( +
+ +
+ )} {!error && !loading && results.length === 0 && } - {!error && !loading && results.length > 0 && <> - - - - {nextURL && - - - - } - } - - - + {!error && !loading && results.length > 0 && ( + <> +
+ +
+ {nextURL && ( +
+ +
+ )} + + )} +
+
+
) } diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 000000000..33ad091d2 --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/scripts/now-preview.sh b/scripts/now-preview.sh deleted file mode 100755 index ccd475748..000000000 --- a/scripts/now-preview.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/bash -set -ex - -now --token ${NOW_TOKEN} rm -y ooni-explorer || echo "nothing deleted" -NOW_URL=$(now --token ${NOW_TOKEN} ./ -n ooni-explorer --public --npm) -now --token ${NOW_TOKEN} alias $NOW_URL ooni-explorer-preview.now.sh -echo "Preview available at: $NOW_URL" diff --git a/services/dayjs.js b/services/dayjs.js index ddb1514b6..9172b7e55 100644 --- a/services/dayjs.js +++ b/services/dayjs.js @@ -1,8 +1,8 @@ import dayjs from 'dayjs' -import utc from 'dayjs/plugin/utc' -import relativeTime from 'dayjs/plugin/relativeTime' -import isSameOrBefore from 'dayjs/plugin/isSameOrBefore' import customParseFormat from 'dayjs/plugin/customParseFormat' +import isSameOrBefore from 'dayjs/plugin/isSameOrBefore' +import relativeTime from 'dayjs/plugin/relativeTime' +import utc from 'dayjs/plugin/utc' import('dayjs/locale/de') import('dayjs/locale/es') import('dayjs/locale/fa') @@ -18,4 +18,4 @@ dayjs .extend(isSameOrBefore) .extend(customParseFormat) -export default dayjs \ No newline at end of file +export default dayjs diff --git a/services/fetchers.js b/services/fetchers.js index e7d07a700..42ceed4f1 100644 --- a/services/fetchers.js +++ b/services/fetchers.js @@ -2,32 +2,41 @@ import axios from 'axios' // import { axiosResponseTime } from 'components/axios-plugins' const baseURL = process.env.NEXT_PUBLIC_OONI_API -export const client = axios.create({baseURL}) +export const client = axios.create({ baseURL }) export const MATFetcher = (query) => { const reqUrl = `${baseURL}/api/v1/aggregation?${query}` - return axios.get(reqUrl).then(r => { - if (!r?.data?.result) { - const error = new Error(`Request ${reqUrl} did not contain expected result`) - error.data = r - throw error - } - return { - data: r.data.result, - loadTime: r.loadTime, - url: r.config.url - } - }).catch(e => { - console.log(e) - e.message = e?.request?.response ?? e.message - throw e - }) + return axios + .get(reqUrl) + .then((r) => { + if (!r?.data?.result) { + const error = new Error( + `Request ${reqUrl} did not contain expected result`, + ) + error.data = r + throw error + } + return { + data: r.data.result, + loadTime: r.loadTime, + url: r.config.url, + } + }) + .catch((e) => { + console.log(e) + e.message = e?.request?.response ?? e.message + throw e + }) } -export const simpleFetcher = (url, params) => (client.get(url, { params }).then(res => res.data.results)) +export const simpleFetcher = (url, params) => + client.get(url, { params }).then((res) => res.data.results) -export const fetcherWithPreprocessing = ([url, {params, resultKey = 'results', preprocessFn}]) => { - return client.get(url, { params }).then(res => { +export const fetcherWithPreprocessing = ([ + url, + { params, resultKey = 'results', preprocessFn }, +]) => { + return client.get(url, { params }).then((res) => { if (preprocessFn) return preprocessFn(res.data[resultKey]) return res.data[resultKey] }) diff --git a/styles/globals.css b/styles/globals.css new file mode 100644 index 000000000..2597087a5 --- /dev/null +++ b/styles/globals.css @@ -0,0 +1,54 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +.btn { + @apply flex items-center px-6 py-2 text-base justify-center rounded-full font-medium border-2 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-white focus:ring-offset-1 disabled:opacity-60 disabled:pointer-events-none; +} + +.btn.btn-primary { + @apply bg-blue-500 text-white; +} + +.btn.btn-primary-hollow { + @apply bg-transparent text-blue-500 border-blue-500 hover:text-blue-900 hover:border-blue-900; +} + +.btn.btn-dark { + @apply bg-black text-white; +} + +.btn.btn-dark-hollow { + @apply bg-transparent text-black hover:text-black hover:text-white hover:bg-gray-100 hover:border-black; +} + +.btn.btn-white { + @apply bg-white text-black; +} + +.btn.btn-white-hollow { + @apply bg-transparent text-white hover:text-white hover:text-blue-500 hover:bg-gray-100 hover:border-white; +} + +.btn.btn-sm { + @apply px-4 py-1 text-sm; +} + +.btn.btn-lg { + @apply px-8 py-2 text-2xl; +} + +.btn.btn-xl { + @apply px-16 py-3.5 text-2xl; +} + +@layer base { + a { + @apply text-blue-500 hover:text-blue-800; + } + + * { + text-rendering: geometricPrecision; + box-sizing: border-box; + } +} diff --git a/tailwind.config.js b/tailwind.config.js new file mode 100644 index 000000000..6f0c043ff --- /dev/null +++ b/tailwind.config.js @@ -0,0 +1,49 @@ +/** @type {import('tailwindcss').Config} */ +import { theme } from 'ooni-components' +const plugin = require('tailwindcss/plugin') + +module.exports = { + content: [ + './pages/**/*.{js,ts,jsx,tsx,mdx}', + './components/**/*.{js,ts,jsx,tsx,mdx}', + './node_modules/ooni-components/dist/**/*.{js,ts,jsx,tsx,mdx}', + ], + safelist: ['lg:grid-cols-4', 'lg:grid-cols-6'], + theme, + plugins: [ + plugin(({ addBase, theme }) => { + addBase({ + h1: { + fontSize: theme('fontSize.5xl'), + fontWeight: theme('fontWeight.light'), + lineHeight: theme('lineHeight.normal'), + }, + h2: { + fontSize: theme('fontSize.4xl'), + fontWeight: theme('fontWeight.light'), + lineHeight: theme('lineHeight.normal'), + }, + h3: { + fontSize: theme('fontSize.2xl'), + fontWeight: theme('fontWeight.semibold'), + lineHeight: theme('lineHeight.normal'), + }, + h4: { + fontSize: theme('fontSize.xl'), + fontWeight: theme('fontWeight.semibold'), + lineHeight: theme('lineHeight.normal'), + }, + h5: { + fontSize: theme('fontSize.base'), + fontWeight: theme('fontWeight.semibold'), + lineHeight: theme('lineHeight.normal'), + }, + h6: { + fontSize: theme('fontSize.xs'), + fontWeight: theme('fontWeight.semibold'), + lineHeight: theme('lineHeight.normal'), + }, + }) + }), + ], +} diff --git a/yarn.lock b/yarn.lock index d1197f1f2..11c892155 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,6 +2,11 @@ # yarn lockfile v1 +"@alloc/quick-lru@^5.2.0": + version "5.2.0" + resolved "https://registry.yarnpkg.com/@alloc/quick-lru/-/quick-lru-5.2.0.tgz#7bf68b20c0a350f936915fcae06f58e32007ce30" + integrity sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw== + "@ampproject/remapping@^2.2.0": version "2.3.0" resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.3.0.tgz#ed441b6fa600072520ce18b43d2c8cc8caecc7f4" @@ -1013,7 +1018,7 @@ resolved "https://registry.yarnpkg.com/@babel/regjsgen/-/regjsgen-0.8.0.tgz#f0ba69b075e1f05fb2825b7fad991e7adbb18310" integrity sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA== -"@babel/runtime@^7.0.0", "@babel/runtime@^7.12.0", "@babel/runtime@^7.12.5", "@babel/runtime@^7.14.6", "@babel/runtime@^7.18.3", "@babel/runtime@^7.20.13", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7": +"@babel/runtime@^7.0.0", "@babel/runtime@^7.12.0", "@babel/runtime@^7.12.5", "@babel/runtime@^7.14.6", "@babel/runtime@^7.18.3", "@babel/runtime@^7.20.13", "@babel/runtime@^7.24.1", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.24.7.tgz#f4f0d5530e8dbdf59b3451b9b3e594b6ba082e12" integrity sha512-UwgBRMjJP+xv857DCngvqXI3Iq6J4v0wXmwc6sapg+zyhbwmQX67LUEFrkK5tbyJ30jGuG3ZvWpBiB9LCy1kWw== @@ -1054,59 +1059,59 @@ "@babel/helper-validator-identifier" "^7.24.7" to-fast-properties "^2.0.0" -"@biomejs/biome@^1.7.0": - version "1.8.2" - resolved "https://registry.yarnpkg.com/@biomejs/biome/-/biome-1.8.2.tgz#a2c93113409b863eb043095e1aa85f11bf31af04" - integrity sha512-XafCzLgs0xbH0bCjYKxQ63ig2V86fZQMq1jiy5pyLToWk9aHxA8GAUxyBtklPHtPYZPGEPOYglQHj4jyfUp+Iw== +"@biomejs/biome@^1.8.3": + version "1.8.3" + resolved "https://registry.yarnpkg.com/@biomejs/biome/-/biome-1.8.3.tgz#3b5eecea90d973f71618aae3e6e8be4d2ca23e42" + integrity sha512-/uUV3MV+vyAczO+vKrPdOW0Iaet7UnJMU4bNMinggGJTAnBPjCoLEYcyYtYHNnUNYlv4xZMH6hVIQCAozq8d5w== optionalDependencies: - "@biomejs/cli-darwin-arm64" "1.8.2" - "@biomejs/cli-darwin-x64" "1.8.2" - "@biomejs/cli-linux-arm64" "1.8.2" - "@biomejs/cli-linux-arm64-musl" "1.8.2" - "@biomejs/cli-linux-x64" "1.8.2" - "@biomejs/cli-linux-x64-musl" "1.8.2" - "@biomejs/cli-win32-arm64" "1.8.2" - "@biomejs/cli-win32-x64" "1.8.2" - -"@biomejs/cli-darwin-arm64@1.8.2": - version "1.8.2" - resolved "https://registry.yarnpkg.com/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-1.8.2.tgz#9e1b3c480d7c62d3272a3b6d1e3e8e7143830a94" - integrity sha512-l9msLsTcSIAPqMsPIhodQmb50sEfaXPLQ0YW4cdj6INmd8iaOh/V9NceQb2366vACTJgcWDQ2RzlvURek1T68g== - -"@biomejs/cli-darwin-x64@1.8.2": - version "1.8.2" - resolved "https://registry.yarnpkg.com/@biomejs/cli-darwin-x64/-/cli-darwin-x64-1.8.2.tgz#9d30a25bccbbbd577146642cfbcfb9a96c661708" - integrity sha512-Fc4y/FuIxRSiB3TJ+y27vFDE/HJt4QgBuymktsIKEcBZvnKfsRjxvzVDunccRn4xbKgepnp+fn6BoS+ZIg/I3Q== - -"@biomejs/cli-linux-arm64-musl@1.8.2": - version "1.8.2" - resolved "https://registry.yarnpkg.com/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-1.8.2.tgz#2d87855d8ba3efea19e20f307dafe891a6716a0d" - integrity sha512-WpT41QJJvkZa1eZq0WmD513zkC6AYaMI39HJKmKeiUeX2NZirG+bxv1YRDhqkns1NbBqo3+qrJqBkPmOW+xAVA== - -"@biomejs/cli-linux-arm64@1.8.2": - version "1.8.2" - resolved "https://registry.yarnpkg.com/@biomejs/cli-linux-arm64/-/cli-linux-arm64-1.8.2.tgz#661cc18ca82428622487abf146bdaa98d978f9e4" - integrity sha512-Q99qwP0qibkZxm2kfnt37OxeIlliDYf5ogi3zX9ij2DULzc+KtPA9Uj0wCljcJofOBsBYaHc7597Q+Bf/251ww== - -"@biomejs/cli-linux-x64-musl@1.8.2": - version "1.8.2" - resolved "https://registry.yarnpkg.com/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-1.8.2.tgz#18debe553137b554e0e14778c52af53eff5b34c6" - integrity sha512-rk1Wj4d3LIlAlIAS1m2jlyfOjkNbuY1lfwKvWIAeZC51yDMzwhRD7cReE5PE+jqLDtq60PX38hDPeKd7nA1S6A== - -"@biomejs/cli-linux-x64@1.8.2": - version "1.8.2" - resolved "https://registry.yarnpkg.com/@biomejs/cli-linux-x64/-/cli-linux-x64-1.8.2.tgz#5c6bd97b4740491d597830b120d98ed45b55ba20" - integrity sha512-bjhhUVFchFid2gOjrvBe4fg8BShcpyFQTHuB/QQnfGxs1ddrGP30yq3fHfc6S6MoCcz9Tjd3Zzq1EfWfyy5iHA== - -"@biomejs/cli-win32-arm64@1.8.2": - version "1.8.2" - resolved "https://registry.yarnpkg.com/@biomejs/cli-win32-arm64/-/cli-win32-arm64-1.8.2.tgz#d703a3162d0f8a68686d2420a473ba4ac0acc925" - integrity sha512-EUbqmCmNWT5xhnxHrCAEBzJB1AnLqxTYoRjlxiCMzGvsy5jQzhCanJ8CT9kNsApW3pfPWBWkoTa7qrwWmwnEGA== - -"@biomejs/cli-win32-x64@1.8.2": - version "1.8.2" - resolved "https://registry.yarnpkg.com/@biomejs/cli-win32-x64/-/cli-win32-x64-1.8.2.tgz#d4522f1aa7f579d3524e23fed5f307a231e43f31" - integrity sha512-n9H5oRUCk1uNezMgyJh9+hZdtfD8PXLLeq8DUzTycIhl0I1BulIoZ/uxWgRVDFDwAR1JHu1AykISCRFNGnc4iA== + "@biomejs/cli-darwin-arm64" "1.8.3" + "@biomejs/cli-darwin-x64" "1.8.3" + "@biomejs/cli-linux-arm64" "1.8.3" + "@biomejs/cli-linux-arm64-musl" "1.8.3" + "@biomejs/cli-linux-x64" "1.8.3" + "@biomejs/cli-linux-x64-musl" "1.8.3" + "@biomejs/cli-win32-arm64" "1.8.3" + "@biomejs/cli-win32-x64" "1.8.3" + +"@biomejs/cli-darwin-arm64@1.8.3": + version "1.8.3" + resolved "https://registry.yarnpkg.com/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-1.8.3.tgz#be2bfdd445cd2d3cb0ff41a96a72ec761753997c" + integrity sha512-9DYOjclFpKrH/m1Oz75SSExR8VKvNSSsLnVIqdnKexj6NwmiMlKk94Wa1kZEdv6MCOHGHgyyoV57Cw8WzL5n3A== + +"@biomejs/cli-darwin-x64@1.8.3": + version "1.8.3" + resolved "https://registry.yarnpkg.com/@biomejs/cli-darwin-x64/-/cli-darwin-x64-1.8.3.tgz#47d408edd9f5c04069fbcf8610bacf1db8c6c0d9" + integrity sha512-UeW44L/AtbmOF7KXLCoM+9PSgPo0IDcyEUfIoOXYeANaNXXf9mLUwV1GeF2OWjyic5zj6CnAJ9uzk2LT3v/wAw== + +"@biomejs/cli-linux-arm64-musl@1.8.3": + version "1.8.3" + resolved "https://registry.yarnpkg.com/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-1.8.3.tgz#44df284383d57cf4f28daeedd080dad7be05df78" + integrity sha512-9yjUfOFN7wrYsXt/T/gEWfvVxKlnh3yBpnScw98IF+oOeCYb5/b/+K7YNqKROV2i1DlMjg9g/EcN9wvj+NkMuQ== + +"@biomejs/cli-linux-arm64@1.8.3": + version "1.8.3" + resolved "https://registry.yarnpkg.com/@biomejs/cli-linux-arm64/-/cli-linux-arm64-1.8.3.tgz#6a6b1da1dfce0294a028cbb5d6c40d73691dd713" + integrity sha512-fed2ji8s+I/m8upWpTJGanqiJ0rnlHOK3DdxsyVLZQ8ClY6qLuPc9uehCREBifRJLl/iJyQpHIRufLDeotsPtw== + +"@biomejs/cli-linux-x64-musl@1.8.3": + version "1.8.3" + resolved "https://registry.yarnpkg.com/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-1.8.3.tgz#ceef30a8ee1a00d4ad31e32dd31ba2a661f2719d" + integrity sha512-UHrGJX7PrKMKzPGoEsooKC9jXJMa28TUSMjcIlbDnIO4EAavCoVmNQaIuUSH0Ls2mpGMwUIf+aZJv657zfWWjA== + +"@biomejs/cli-linux-x64@1.8.3": + version "1.8.3" + resolved "https://registry.yarnpkg.com/@biomejs/cli-linux-x64/-/cli-linux-x64-1.8.3.tgz#665df74d19fb8f83001a9d80824d3a1723e2123f" + integrity sha512-I8G2QmuE1teISyT8ie1HXsjFRz9L1m5n83U1O6m30Kw+kPMPSKjag6QGUn+sXT8V+XWIZxFFBoTDEDZW2KPDDw== + +"@biomejs/cli-win32-arm64@1.8.3": + version "1.8.3" + resolved "https://registry.yarnpkg.com/@biomejs/cli-win32-arm64/-/cli-win32-arm64-1.8.3.tgz#0fb6f58990f4de0331a6ed22c47c66f5a89133cc" + integrity sha512-J+Hu9WvrBevfy06eU1Na0lpc7uR9tibm9maHynLIoAjLZpQU3IW+OKHUtyL8p6/3pT2Ju5t5emReeIS2SAxhkQ== + +"@biomejs/cli-win32-x64@1.8.3": + version "1.8.3" + resolved "https://registry.yarnpkg.com/@biomejs/cli-win32-x64/-/cli-win32-x64-1.8.3.tgz#6a9dc5a4e13357277da43c015cd5cdc374035448" + integrity sha512-/PJ59vA1pnQeKahemaQf4Nyj7IKUvGQSc3Ze1uIGi+Wvr1xF7rGobSrAAG01T/gUDG21vkDsZYM03NAmPiVkqg== "@bundled-es-modules/cookie@^2.0.0": version "2.0.0" @@ -1202,30 +1207,6 @@ resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.9.1.tgz#4ffb0055f7ef676ebc3a5a91fb621393294e2f43" integrity sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ== -"@emotion/is-prop-valid@1.2.2": - version "1.2.2" - resolved "https://registry.yarnpkg.com/@emotion/is-prop-valid/-/is-prop-valid-1.2.2.tgz#d4175076679c6a26faa92b03bb786f9e52612337" - integrity sha512-uNsoYd37AFmaCdXlg6EYD1KaPOaRWRByMCYzbKUX4+hhMfrxdVSelShywL4JVaAeM/eHUOSprYBQls+/neX3pw== - dependencies: - "@emotion/memoize" "^0.8.1" - -"@emotion/is-prop-valid@^0.8.1": - version "0.8.8" - resolved "https://registry.yarnpkg.com/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz#db28b1c4368a259b60a97311d6a952d4fd01ac1a" - integrity sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA== - dependencies: - "@emotion/memoize" "0.7.4" - -"@emotion/memoize@0.7.4": - version "0.7.4" - resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.7.4.tgz#19bf0f5af19149111c40d98bb0cf82119f5d9eeb" - integrity sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw== - -"@emotion/memoize@^0.7.1": - version "0.7.5" - resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.7.5.tgz#2c40f81449a4e554e9fc6396910ed4843ec2be50" - integrity sha512-igX9a37DR2ZPGYtV6suZ6whr8pTFtyHL3K/oLUotxpSVO2ASaprmAe2Dkq7tBo7CRY7MMDrAa9nuQP9/YG8FxQ== - "@emotion/memoize@^0.8.1": version "0.8.1" resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.8.1.tgz#c1ddb040429c6d21d38cc945fe75c818cfb68e17" @@ -1261,7 +1242,7 @@ resolved "https://registry.yarnpkg.com/@emotion/sheet/-/sheet-1.2.2.tgz#d58e788ee27267a14342303e1abb3d508b6d0fec" integrity sha512-0QBtGvaqtWi+nx6doRwDdBIzhNdZrXUppvTM4dtZZWEGTXL/XE/yJxLMGlDT1Gt+UHH5IX1n+jkXyytE/av7OA== -"@emotion/unitless@0.8.1", "@emotion/unitless@^0.8.1": +"@emotion/unitless@^0.8.1": version "0.8.1" resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.8.1.tgz#182b5a4704ef8ad91bde93f7a860a88fd92c79a3" integrity sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ== @@ -1432,7 +1413,7 @@ resolved "https://registry.yarnpkg.com/@inquirer/type/-/type-1.3.3.tgz#26b2628630fd2381c7fa1e3ab396feb9bbc575da" integrity sha512-xTUt0NulylX27/zMx04ZYar/kr1raaiFTVvQ5feljQsiAgdm0WPj4S73/ye0fbslh+15QrIuDvfCXTek7pMY5A== -"@jridgewell/gen-mapping@^0.3.5": +"@jridgewell/gen-mapping@^0.3.2", "@jridgewell/gen-mapping@^0.3.5": version "0.3.5" resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz#dcce6aff74bdf6dad1a95802b69b04a2fcb1fb36" integrity sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg== @@ -1623,19 +1604,6 @@ d3-scale "^3.2.3" d3-shape "^1.3.5" -"@nivo/heatmap@^0.80.0": - version "0.80.0" - resolved "https://registry.yarnpkg.com/@nivo/heatmap/-/heatmap-0.80.0.tgz#07db42570c8cd5882c907f452ebea6b221256670" - integrity sha512-FZufo3Y79NaO2mTlboGj3kBpNYGEJhgI0WpONkkUwnjqF27FkLZ6geuqBnsRSpK+3vgUQeakd8ZvSBtBHhHUvQ== - dependencies: - "@nivo/annotations" "0.80.0" - "@nivo/axes" "0.80.0" - "@nivo/colors" "0.80.0" - "@nivo/legends" "0.80.0" - "@nivo/tooltip" "0.80.0" - "@react-spring/web" "9.4.5" - d3-scale "^3.2.3" - "@nivo/legends@0.80.0": version "0.80.0" resolved "https://registry.yarnpkg.com/@nivo/legends/-/legends-0.80.0.tgz#49edc54000075b4df055f86794a8c32810269d06" @@ -1688,6 +1656,27 @@ d3-delaunay "^5.3.0" d3-scale "^3.2.3" +"@nodelib/fs.scandir@2.1.5": + version "2.1.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" + integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== + dependencies: + "@nodelib/fs.stat" "2.0.5" + run-parallel "^1.1.9" + +"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b" + integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== + +"@nodelib/fs.walk@^1.2.3": + version "1.2.8" + resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a" + integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== + dependencies: + "@nodelib/fs.scandir" "2.1.5" + fastq "^1.6.0" + "@open-draft/deferred-promise@^2.2.0": version "2.2.0" resolved "https://registry.yarnpkg.com/@open-draft/deferred-promise/-/deferred-promise-2.2.0.tgz#4a822d10f6f0e316be4d67b4d4f8c9a124b073bd" @@ -1967,105 +1956,6 @@ "@babel/code-frame" "^7.16.0" chalk "^4.1.0" -"@styled-system/background@^5.1.2": - version "5.1.2" - resolved "https://registry.yarnpkg.com/@styled-system/background/-/background-5.1.2.tgz#75c63d06b497ab372b70186c0bf608d62847a2ba" - integrity sha512-jtwH2C/U6ssuGSvwTN3ri/IyjdHb8W9X/g8Y0JLcrH02G+BW3OS8kZdHphF1/YyRklnrKrBT2ngwGUK6aqqV3A== - dependencies: - "@styled-system/core" "^5.1.2" - -"@styled-system/border@^5.1.5": - version "5.1.5" - resolved "https://registry.yarnpkg.com/@styled-system/border/-/border-5.1.5.tgz#0493d4332d2b59b74bb0d57d08c73eb555761ba6" - integrity sha512-JvddhNrnhGigtzWRCVuAHepniyVi6hBlimxWDVAdcTuk7aRn9BYJUwfHslURtwYFsF5FoEs8Zmr1oZq2M1AP0A== - dependencies: - "@styled-system/core" "^5.1.2" - -"@styled-system/color@^5.1.2": - version "5.1.2" - resolved "https://registry.yarnpkg.com/@styled-system/color/-/color-5.1.2.tgz#b8d6b4af481faabe4abca1a60f8daa4ccc2d9f43" - integrity sha512-1kCkeKDZkt4GYkuFNKc7vJQMcOmTl3bJY3YBUs7fCNM6mMYJeT1pViQ2LwBSBJytj3AB0o4IdLBoepgSgGl5MA== - dependencies: - "@styled-system/core" "^5.1.2" - -"@styled-system/core@^5.1.2": - version "5.1.2" - resolved "https://registry.yarnpkg.com/@styled-system/core/-/core-5.1.2.tgz#b8b7b86455d5a0514f071c4fa8e434b987f6a772" - integrity sha512-XclBDdNIy7OPOsN4HBsawG2eiWfCcuFt6gxKn1x4QfMIgeO6TOlA2pZZ5GWZtIhCUqEPTgIBta6JXsGyCkLBYw== - dependencies: - object-assign "^4.1.1" - -"@styled-system/css@^5.1.5": - version "5.1.5" - resolved "https://registry.yarnpkg.com/@styled-system/css/-/css-5.1.5.tgz#0460d5f3ff962fa649ea128ef58d9584f403bbbc" - integrity sha512-XkORZdS5kypzcBotAMPBoeckDs9aSZVkvrAlq5K3xP8IMAUek+x2O4NtwoSgkYkWWzVBu6DGdFZLR790QWGG+A== - -"@styled-system/flexbox@^5.1.2": - version "5.1.2" - resolved "https://registry.yarnpkg.com/@styled-system/flexbox/-/flexbox-5.1.2.tgz#077090f43f61c3852df63da24e4108087a8beecf" - integrity sha512-6hHV52+eUk654Y1J2v77B8iLeBNtc+SA3R4necsu2VVinSD7+XY5PCCEzBFaWs42dtOEDIa2lMrgL0YBC01mDQ== - dependencies: - "@styled-system/core" "^5.1.2" - -"@styled-system/grid@^5.1.2": - version "5.1.2" - resolved "https://registry.yarnpkg.com/@styled-system/grid/-/grid-5.1.2.tgz#7165049877732900b99cd00759679fbe45c6c573" - integrity sha512-K3YiV1KyHHzgdNuNlaw8oW2ktMuGga99o1e/NAfTEi5Zsa7JXxzwEnVSDSBdJC+z6R8WYTCYRQC6bkVFcvdTeg== - dependencies: - "@styled-system/core" "^5.1.2" - -"@styled-system/layout@^5.1.2": - version "5.1.2" - resolved "https://registry.yarnpkg.com/@styled-system/layout/-/layout-5.1.2.tgz#12d73e79887e10062f4dbbbc2067462eace42339" - integrity sha512-wUhkMBqSeacPFhoE9S6UF3fsMEKFv91gF4AdDWp0Aym1yeMPpqz9l9qS/6vjSsDPF7zOb5cOKC3tcKKOMuDCPw== - dependencies: - "@styled-system/core" "^5.1.2" - -"@styled-system/position@^5.1.2": - version "5.1.2" - resolved "https://registry.yarnpkg.com/@styled-system/position/-/position-5.1.2.tgz#56961266566836f57a24d8e8e33ce0c1adb59dd3" - integrity sha512-60IZfMXEOOZe3l1mCu6sj/2NAyUmES2kR9Kzp7s2D3P4qKsZWxD1Se1+wJvevb+1TP+ZMkGPEYYXRyU8M1aF5A== - dependencies: - "@styled-system/core" "^5.1.2" - -"@styled-system/shadow@^5.1.2": - version "5.1.2" - resolved "https://registry.yarnpkg.com/@styled-system/shadow/-/shadow-5.1.2.tgz#beddab28d7de03cd0177a87ac4ed3b3b6d9831fd" - integrity sha512-wqniqYb7XuZM7K7C0d1Euxc4eGtqEe/lvM0WjuAFsQVImiq6KGT7s7is+0bNI8O4Dwg27jyu4Lfqo/oIQXNzAg== - dependencies: - "@styled-system/core" "^5.1.2" - -"@styled-system/should-forward-prop@^5.1.5": - version "5.1.5" - resolved "https://registry.yarnpkg.com/@styled-system/should-forward-prop/-/should-forward-prop-5.1.5.tgz#c392008c6ae14a6eb78bf1932733594f7f7e5c76" - integrity sha512-+rPRomgCGYnUIaFabDoOgpSDc4UUJ1KsmlnzcEp0tu5lFrBQKgZclSo18Z1URhaZm7a6agGtS5Xif7tuC2s52Q== - dependencies: - "@emotion/is-prop-valid" "^0.8.1" - "@emotion/memoize" "^0.7.1" - styled-system "^5.1.5" - -"@styled-system/space@^5.1.2": - version "5.1.2" - resolved "https://registry.yarnpkg.com/@styled-system/space/-/space-5.1.2.tgz#38925d2fa29a41c0eb20e65b7c3efb6e8efce953" - integrity sha512-+zzYpR8uvfhcAbaPXhH8QgDAV//flxqxSjHiS9cDFQQUSznXMQmxJegbhcdEF7/eNnJgHeIXv1jmny78kipgBA== - dependencies: - "@styled-system/core" "^5.1.2" - -"@styled-system/typography@^5.1.2": - version "5.1.2" - resolved "https://registry.yarnpkg.com/@styled-system/typography/-/typography-5.1.2.tgz#65fb791c67d50cd2900d234583eaacdca8c134f7" - integrity sha512-BxbVUnN8N7hJ4aaPOd7wEsudeT7CxarR+2hns8XCX1zp0DFfbWw4xYa/olA0oQaqx7F1hzDg+eRaGzAJbF+jOg== - dependencies: - "@styled-system/core" "^5.1.2" - -"@styled-system/variant@^5.1.5": - version "5.1.5" - resolved "https://registry.yarnpkg.com/@styled-system/variant/-/variant-5.1.5.tgz#8446d8aad06af3a4c723d717841df2dbe4ddeafd" - integrity sha512-Yn8hXAFoWIro8+Q5J8YJd/mP85Teiut3fsGVR9CAxwgNfIAiqlYxsk5iHU7VHJks/0KjL4ATSjmbtCDC/4l1qw== - dependencies: - "@styled-system/core" "^5.1.2" - "@styled-system/css" "^5.1.5" - "@svgr/babel-plugin-add-jsx-attribute@8.0.0": version "8.0.0" resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-8.0.0.tgz#4001f5d5dd87fa13303e36ee106e3ff3a7eb8b22" @@ -2301,11 +2191,6 @@ resolved "https://registry.yarnpkg.com/@types/statuses/-/statuses-2.0.5.tgz#f61ab46d5352fd73c863a1ea4e1cef3b0b51ae63" integrity sha512-jmIUGWrAiwu3dZpxntxieC+1n/5c3mjrImkmOSQ2NC5uP6cYO4aAZDdSmRcI5C1oiTmqlZGHC+/NmJrKogbP5A== -"@types/stylis@4.2.5": - version "4.2.5" - resolved "https://registry.yarnpkg.com/@types/stylis/-/stylis-4.2.5.tgz#1daa6456f40959d06157698a653a9ab0a70281df" - integrity sha512-1Xve+NMN7FWjY14vLoY5tL3BVEQ/n42YLwaqJIPYhotZ9uBHt87VceMwWQpzmdEt2TNXIorIFG+YeCUUW7RInw== - "@types/wrap-ansi@^3.0.0": version "3.0.0" resolved "https://registry.yarnpkg.com/@types/wrap-ansi/-/wrap-ansi-3.0.0.tgz#18b97a972f94f60a679fd5c796d96421b9abb9fd" @@ -2420,6 +2305,19 @@ ansi-styles@^5.0.0: resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b" integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== +any-promise@^1.0.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f" + integrity sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A== + +anymatch@~3.1.2: + version "3.1.3" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" + integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + arch@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/arch/-/arch-2.2.0.tgz#1bc47818f305764f23ab3306b0bfc086c5a29d11" @@ -2512,6 +2410,18 @@ at-least-node@^1.0.0: resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2" integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg== +autoprefixer@^10.4.19: + version "10.4.19" + resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.19.tgz#ad25a856e82ee9d7898c59583c1afeb3fa65f89f" + integrity sha512-BaENR2+zBZ8xXhM4pUaKUxlVdxZ0EZhjvbopwnXmxRUfqDmwSpC2lAi/QXvx7NRdPCo1WKEcEF6mV64si1z4Ew== + dependencies: + browserslist "^4.23.0" + caniuse-lite "^1.0.30001599" + fraction.js "^4.3.7" + normalize-range "^0.1.2" + picocolors "^1.0.0" + postcss-value-parser "^4.2.0" + available-typed-arrays@^1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz#a5cc375d6a03c2efc87a553f3e0b1522def14846" @@ -2600,6 +2510,11 @@ bidi-js@^1.0.3: dependencies: require-from-string "^2.0.2" +binary-extensions@^2.0.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522" + integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw== + blob-util@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/blob-util/-/blob-util-2.0.2.tgz#3b4e3c281111bb7f11128518006cdc60b403a1eb" @@ -2622,6 +2537,13 @@ brace-expansion@^2.0.1: dependencies: balanced-match "^1.0.0" +braces@^3.0.3, braces@~3.0.2: + version "3.0.3" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" + integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== + dependencies: + fill-range "^7.1.1" + browserslist@^4.22.2, browserslist@^4.23.0: version "4.23.1" resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.23.1.tgz#ce4af0534b3d37db5c1a4ca98b9080f985041e96" @@ -2678,17 +2600,17 @@ callsites@^3.0.0: resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== +camelcase-css@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/camelcase-css/-/camelcase-css-2.0.1.tgz#ee978f6947914cc30c6b44741b6ed1df7f043fd5" + integrity sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA== + camelcase@^6.2.0: version "6.3.0" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== -camelize@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/camelize/-/camelize-1.0.1.tgz#89b7e16884056331a35d6b5ad064332c91daa6c3" - integrity sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ== - -caniuse-lite@^1.0.30001579, caniuse-lite@^1.0.30001629: +caniuse-lite@^1.0.30001579, caniuse-lite@^1.0.30001599, caniuse-lite@^1.0.30001629: version "1.0.30001636" resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001636.tgz#b15f52d2bdb95fad32c2f53c0b68032b85188a78" integrity sha512-bMg2vmr8XBsbL6Lr0UHXy/21m84FTxDLWn2FSqMd5PrlbMxwJlQnC2YWYxVgp66PZE+BBNF2jYQUBKCo1FDeZg== @@ -2728,11 +2650,33 @@ check-more-types@2.24.0, check-more-types@^2.24.0: resolved "https://registry.yarnpkg.com/check-more-types/-/check-more-types-2.24.0.tgz#1420ffb10fd444dcfc79b43891bbfffd32a84600" integrity sha512-Pj779qHxV2tuapviy1bSZNEL1maXr13bPYpsvSDB68HlYcYuhlDrmGd63i0JHMCLKzc7rUSNIrpdJlhVlNwrxA== +chokidar@^3.5.3: + version "3.6.0" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b" + integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw== + dependencies: + anymatch "~3.1.2" + braces "~3.0.2" + glob-parent "~5.1.2" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.6.0" + optionalDependencies: + fsevents "~2.3.2" + ci-info@^3.2.0: version "3.9.0" resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.9.0.tgz#4279a62028a7b1f262f3473fc9605f5e218c59b4" integrity sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ== +class-variance-authority@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/class-variance-authority/-/class-variance-authority-0.7.0.tgz#1c3134d634d80271b1837452b06d821915954522" + integrity sha512-jFI8IQw4hczaL4ALINxqLEXQbWcNjoSkloa4IaufXCJr6QawJyw7tuRysRsrE8w2p/4gGaxKIt/hX3qz/IbD1A== + dependencies: + clsx "2.0.0" + clean-stack@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" @@ -2791,12 +2735,17 @@ cliui@^8.0.1: strip-ansi "^6.0.1" wrap-ansi "^7.0.0" +clsx@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.0.0.tgz#12658f3fd98fafe62075595a5c30e43d18f3d00b" + integrity sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q== + clsx@^1.0.4: version "1.2.1" resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12" integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg== -clsx@^2.1.0: +clsx@^2.1.0, clsx@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.1.tgz#eed397c9fd8bd882bfb18deab7102049a2f32999" integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA== @@ -2837,6 +2786,11 @@ combined-stream@^1.0.6, combined-stream@^1.0.8, combined-stream@~1.0.6: dependencies: delayed-stream "~1.0.0" +commander@^4.0.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068" + integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA== + commander@^6.2.1: version "6.2.1" resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c" @@ -2936,11 +2890,6 @@ cross-spawn@^7.0.0, cross-spawn@^7.0.3: shebang-command "^2.0.0" which "^2.0.1" -css-color-keywords@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/css-color-keywords/-/css-color-keywords-1.0.0.tgz#fea2616dc676b2962686b3af8dbdbe180b244e05" - integrity sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg== - css-select@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/css-select/-/css-select-5.1.0.tgz#b8ebd6554c3637ccc76688804ad3f6a6fdaea8a6" @@ -2952,15 +2901,6 @@ css-select@^5.1.0: domutils "^3.0.1" nth-check "^2.0.1" -css-to-react-native@3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/css-to-react-native/-/css-to-react-native-3.2.0.tgz#cdd8099f71024e149e4f6fe17a7d46ecd55f1e32" - integrity sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ== - dependencies: - camelize "^1.0.0" - css-color-keywords "^1.0.0" - postcss-value-parser "^4.0.2" - css-tree@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-2.3.1.tgz#10264ce1e5442e8572fc82fbe490644ff54b5c20" @@ -2982,6 +2922,11 @@ css-what@^6.1.0: resolved "https://registry.yarnpkg.com/css-what/-/css-what-6.1.0.tgz#fb5effcf76f1ddea2c81bdfaa4de44e79bac70f4" integrity sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw== +cssesc@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" + integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== + csso@^5.0.5: version "5.0.5" resolved "https://registry.yarnpkg.com/csso/-/csso-5.0.5.tgz#f9b7fe6cc6ac0b7d90781bb16d5e9874303e2ca6" @@ -2996,7 +2941,7 @@ cssstyle@^4.0.1: dependencies: rrweb-cssom "^0.6.0" -csstype@3.1.3, csstype@^3.0.2: +csstype@^3.0.2: version "3.1.3" resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81" integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw== @@ -3313,6 +3258,16 @@ dequal@^2.0.3: resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be" integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA== +didyoumean@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/didyoumean/-/didyoumean-1.2.2.tgz#989346ffe9e839b4555ecf5666edea0d3e8ad037" + integrity sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw== + +dlv@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/dlv/-/dlv-1.1.3.tgz#5c198a8a11453596e751494d49874bc7732f2e79" + integrity sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA== + document.contains@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/document.contains/-/document.contains-1.0.2.tgz#4260abad67a6ae9e135c1be83d68da0db169d5f0" @@ -3628,6 +3583,24 @@ fast-deep-equal@^3.1.3: resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== +fast-glob@^3.3.0: + version "3.3.2" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129" + integrity sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow== + dependencies: + "@nodelib/fs.stat" "^2.0.2" + "@nodelib/fs.walk" "^1.2.3" + glob-parent "^5.1.2" + merge2 "^1.3.0" + micromatch "^4.0.4" + +fastq@^1.6.0: + version "1.17.1" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.17.1.tgz#2a523f07a4e7b1e81a42b91b8bf2254107753b47" + integrity sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w== + dependencies: + reusify "^1.0.4" + fbemitter@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/fbemitter/-/fbemitter-3.0.0.tgz#00b2a1af5411254aab416cd75f9e6289bee4bff3" @@ -3667,6 +3640,13 @@ figures@^3.2.0: dependencies: escape-string-regexp "^1.0.5" +fill-range@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" + integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== + dependencies: + to-regex-range "^5.0.1" + find-root@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/find-root/-/find-root-1.1.0.tgz#abcfc8ba76f708c42a97b3d685b7e9450bfb9ce4" @@ -3728,6 +3708,11 @@ form-data@~2.3.2: combined-stream "^1.0.6" mime-types "^2.1.12" +fraction.js@^4.3.7: + version "4.3.7" + resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.3.7.tgz#06ca0085157e42fda7f9e726e79fefc4068840f7" + integrity sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew== + from@~0: version "0.1.7" resolved "https://registry.yarnpkg.com/from/-/from-0.1.7.tgz#83c60afc58b9c56997007ed1a768b3ab303a44fe" @@ -3829,6 +3814,20 @@ getpass@^0.1.1: dependencies: assert-plus "^1.0.0" +glob-parent@^5.1.2, glob-parent@~5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== + dependencies: + is-glob "^4.0.1" + +glob-parent@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3" + integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== + dependencies: + is-glob "^4.0.3" + glob@^10.0.0, glob@^10.3.10: version "10.4.2" resolved "https://registry.yarnpkg.com/glob/-/glob-10.4.2.tgz#bed6b95dade5c1f80b4434daced233aee76160e5" @@ -4150,6 +4149,13 @@ is-bigint@^1.0.1: dependencies: has-bigints "^1.0.1" +is-binary-path@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" + integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== + dependencies: + binary-extensions "^2.0.0" + is-boolean-object@^1.1.0: version "1.1.2" resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.1.2.tgz#5c6dc200246dd9321ae4b885a114bb1f75f63719" @@ -4191,11 +4197,23 @@ is-date-object@^1.0.1: dependencies: has-tostringtag "^1.0.0" +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== + is-fullwidth-code-point@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== +is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: + version "4.0.3" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== + dependencies: + is-extglob "^2.1.1" + is-installed-globally@~0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/is-installed-globally/-/is-installed-globally-0.4.0.tgz#9a0fd407949c30f86eb6959ef1b7994ed0b7b520" @@ -4221,6 +4239,11 @@ is-number-object@^1.0.4: dependencies: has-tostringtag "^1.0.0" +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + is-path-inside@^3.0.2: version "3.0.3" resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" @@ -4335,6 +4358,11 @@ jackspeak@2.1.1, jackspeak@^3.1.2: optionalDependencies: "@pkgjs/parseargs" "^0.11.0" +jiti@^1.21.0: + version "1.21.6" + resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.21.6.tgz#6c7f7398dd4b3142767f9a168af2f317a428d268" + integrity sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w== + joi@^17.11.0: version "17.13.3" resolved "https://registry.yarnpkg.com/joi/-/joi-17.13.3.tgz#0f5cc1169c999b30d344366d384b12d92558bcec" @@ -4466,6 +4494,16 @@ lie@3.1.1: dependencies: immediate "~3.0.5" +lilconfig@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.1.0.tgz#78e23ac89ebb7e1bfbf25b18043de756548e7f52" + integrity sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ== + +lilconfig@^3.0.0: + version "3.1.2" + resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-3.1.2.tgz#e4a7c3cb549e3a606c8dcc32e5ae1005e62c05cb" + integrity sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow== + lines-and-columns@^1.1.6: version "1.2.4" resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" @@ -4608,6 +4646,19 @@ merge-stream@^2.0.0: resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== +merge2@^1.3.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" + integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== + +micromatch@^4.0.4, micromatch@^4.0.5: + version "4.0.7" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.7.tgz#33e8190d9fe474a9895525f5618eee136d46c2e5" + integrity sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q== + dependencies: + braces "^3.0.3" + picomatch "^2.3.1" + mime-db@1.52.0: version "1.52.0" resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" @@ -4625,6 +4676,11 @@ mimic-fn@^2.1.0: resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== +mini-svg-data-uri@^1.4.4: + version "1.4.4" + resolved "https://registry.yarnpkg.com/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz#8ab0aabcdf8c29ad5693ca595af19dd2ead09939" + integrity sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg== + minimatch@^5.0.1: version "5.1.6" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96" @@ -4704,6 +4760,15 @@ mute-stream@^1.0.0: resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-1.0.0.tgz#e31bd9fe62f0aed23520aa4324ea6671531e013e" integrity sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA== +mz@^2.7.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/mz/-/mz-2.7.0.tgz#95008057a56cafadc2bc63dde7f9ff6955948e32" + integrity sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q== + dependencies: + any-promise "^1.0.0" + object-assign "^4.0.1" + thenify-all "^1.0.0" + nanoid@^3.3.6, nanoid@^3.3.7: version "3.3.7" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8" @@ -4760,6 +4825,16 @@ nodeify@^1.0.0: is-promise "~1.0.0" promise "~1.3.0" +normalize-path@^3.0.0, normalize-path@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" + integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== + +normalize-range@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942" + integrity sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA== + npm-run-path@^4.0.0, npm-run-path@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" @@ -4779,11 +4854,16 @@ nth-check@^2.0.1: dependencies: boolbase "^1.0.0" -object-assign@^4.1.0, object-assign@^4.1.1: +object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== +object-hash@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-3.0.0.tgz#73f97f753e7baffc0e2cc9d6e079079744ac82e9" + integrity sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw== + object-inspect@^1.13.1: version "1.13.1" resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.1.tgz#b96c6109324ccfef6b12216a956ca4dc2ff94bc2" @@ -4844,15 +4924,16 @@ onetime@^5.1.0, onetime@^5.1.2: dependencies: mimic-fn "^2.1.0" -ooni-components@0.6.0-alpha.8: - version "0.6.0-alpha.8" - resolved "https://registry.yarnpkg.com/ooni-components/-/ooni-components-0.6.0-alpha.8.tgz#ffb338245154d64009ef0c944f8a6aff0cd3aa54" - integrity sha512-zaT7tttCQcVCmtGcE/Z5hPF9KtijpCOViNdLor1btlmrE26UgsKCUXwr3oc00eKbJJXyJl6dBUsQahPjucV5vw== +ooni-components@0.7.0-alpha.1: + version "0.7.0-alpha.1" + resolved "https://registry.yarnpkg.com/ooni-components/-/ooni-components-0.7.0-alpha.1.tgz#44a8585ef61815bef471ba08ac4a396067b8d841" + integrity sha512-LCLa4ih2LWvL1p73EDa50/dYqmiJqxaayKkCPVCmUcE6mCQHSun1wNVCyizab9jZd4i2LK2w9sy80xat8dE9kg== dependencies: - "@styled-system/css" "^5.1.5" - "@styled-system/should-forward-prop" "^5.1.5" + class-variance-authority "^0.7.0" + clsx "^2.1.1" + mini-svg-data-uri "^1.4.4" react-select "^5.8.0" - styled-system "^5.1.5" + tailwind-merge "^2.3.0" opener@^1.5.2: version "1.5.2" @@ -4960,22 +5041,66 @@ picocolors@^1.0.0, picocolors@^1.0.1: resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.1.tgz#a8ad579b571952f0e5d25892de5445bcfe25aaa1" integrity sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew== -picomatch@^2.3.1: +picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== -pify@^2.2.0: +pify@^2.2.0, pify@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" integrity sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog== +pirates@^4.0.1: + version "4.0.6" + resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.6.tgz#3018ae32ecfcff6c29ba2267cbf21166ac1f36b9" + integrity sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg== + possible-typed-array-names@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz#89bb63c6fada2c3e90adc4a647beeeb39cc7bf8f" integrity sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q== -postcss-value-parser@^4.0.2: +postcss-import@^15.1.0: + version "15.1.0" + resolved "https://registry.yarnpkg.com/postcss-import/-/postcss-import-15.1.0.tgz#41c64ed8cc0e23735a9698b3249ffdbf704adc70" + integrity sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew== + dependencies: + postcss-value-parser "^4.0.0" + read-cache "^1.0.0" + resolve "^1.1.7" + +postcss-js@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-js/-/postcss-js-4.0.1.tgz#61598186f3703bab052f1c4f7d805f3991bee9d2" + integrity sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw== + dependencies: + camelcase-css "^2.0.1" + +postcss-load-config@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-4.0.2.tgz#7159dcf626118d33e299f485d6afe4aff7c4a3e3" + integrity sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ== + dependencies: + lilconfig "^3.0.0" + yaml "^2.3.4" + +postcss-nested@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/postcss-nested/-/postcss-nested-6.0.1.tgz#f83dc9846ca16d2f4fa864f16e9d9f7d0961662c" + integrity sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ== + dependencies: + postcss-selector-parser "^6.0.11" + +postcss-selector-parser@^6.0.11: + version "6.1.0" + resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.1.0.tgz#49694cb4e7c649299fea510a29fa6577104bcf53" + integrity sha512-UMz42UD0UY0EApS0ZL9o1XnLhSTtvvvLe5Dc2H2O56fvRZi+KulDyf5ctDhhtYJBGKStV2FL1fy6253cmLgqVQ== + dependencies: + cssesc "^3.0.0" + util-deprecate "^1.0.2" + +postcss-value-parser@^4.0.0, postcss-value-parser@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514" integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== @@ -4989,7 +5114,7 @@ postcss@8.4.31: picocolors "^1.0.0" source-map-js "^1.0.2" -postcss@8.4.38: +postcss@^8.4.23, postcss@^8.4.38: version "8.4.38" resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.38.tgz#b387d533baf2054288e337066d81c6bee9db9e0e" integrity sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A== @@ -5128,6 +5253,11 @@ querystringify@^2.1.1: resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6" integrity sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ== +queue-microtask@^1.2.2: + version "1.2.3" + resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" + integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== + quoted-printable@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/quoted-printable/-/quoted-printable-1.0.1.tgz#9eebf5eb3d11eef022b264fd2d2b6b2bb3b84cc3" @@ -5150,7 +5280,7 @@ react-content-loader@^6.2.1: resolved "https://registry.yarnpkg.com/react-content-loader/-/react-content-loader-6.2.1.tgz#8feb733c2d2495002e1b216f13707f2b5f2a8ead" integrity sha512-6ONbFX+Hi3SHuP66JB8CPvJn372pj+qwltJV0J8z/8MFrq98I1cbFdZuhDWeQXu3CFxiiDTXJn7DFxx2ZvrO7g== -react-day-picker@^8.10.0: +react-day-picker@^8.10.1: version "8.10.1" resolved "https://registry.yarnpkg.com/react-day-picker/-/react-day-picker-8.10.1.tgz#4762ec298865919b93ec09ba69621580835b8e80" integrity sha512-TMx7fNbhLk15eqcMt+7Z7S2KF7mfTId/XJDjKE8f+IUcFn0l08/kI4FiYTL/0yuOLmEcbR4Fwe3GJf/NiiMnPA== @@ -5310,6 +5440,13 @@ react@^18.3.1: dependencies: loose-envify "^1.1.0" +read-cache@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/read-cache/-/read-cache-1.0.0.tgz#e664ef31161166c9751cdbe8dbcf86b5fb58f774" + integrity sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA== + dependencies: + pify "^2.3.0" + readable-stream@1.1.x: version "1.1.14" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9" @@ -5320,6 +5457,13 @@ readable-stream@1.1.x: isarray "0.0.1" string_decoder "~0.10.x" +readdirp@~3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" + integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== + dependencies: + picomatch "^2.2.1" + reflect.ownkeys@^1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/reflect.ownkeys/-/reflect.ownkeys-1.1.4.tgz#3cf21da448f2aff8aba63ca601f65c99482e692c" @@ -5411,7 +5555,7 @@ resolve-from@^4.0.0: resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== -resolve@1.22.8, resolve@^1.14.2, resolve@^1.19.0: +resolve@1.22.8, resolve@^1.1.7, resolve@^1.14.2, resolve@^1.19.0, resolve@^1.22.2: version "1.22.8" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d" integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== @@ -5428,6 +5572,11 @@ restore-cursor@^3.1.0: onetime "^5.1.0" signal-exit "^3.0.2" +reusify@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" + integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== + rfdc@^1.3.0: version "1.4.1" resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.4.1.tgz#778f76c4fb731d93414e8f925fbecf64cce7f6ca" @@ -5445,6 +5594,13 @@ rrweb-cssom@^0.6.0: resolved "https://registry.yarnpkg.com/rrweb-cssom/-/rrweb-cssom-0.6.0.tgz#ed298055b97cbddcdeb278f904857629dec5e0e1" integrity sha512-APM0Gt1KoXBz0iIkkdB/kfvGOwC4UuJFeG/c+yV7wSc7q96cG/kJ0HiYCnzivD9SB53cLV1MlHFNfOuPaadYSw== +run-parallel@^1.1.9: + version "1.2.0" + resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" + integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== + dependencies: + queue-microtask "^1.2.2" + rxjs@^7.5.1, rxjs@^7.8.1: version "7.8.1" resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.1.tgz#6f6f3d99ea8044291efd92e7c7fcf562c4057543" @@ -5537,11 +5693,6 @@ setimmediate@^1.0.5: resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" integrity sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA== -shallowequal@1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/shallowequal/-/shallowequal-1.1.0.tgz#188d521de95b9087404fd4dcb68b13df0ae4e7f8" - integrity sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ== - shebang-command@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" @@ -5743,21 +5894,6 @@ strip-final-newline@^2.0.0: resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== -styled-components@^6.1.8: - version "6.1.11" - resolved "https://registry.yarnpkg.com/styled-components/-/styled-components-6.1.11.tgz#01948e5195bf1d39e57e0a85b41958c80e40cfb8" - integrity sha512-Ui0jXPzbp1phYij90h12ksljKGqF8ncGx+pjrNPsSPhbUUjWT2tD1FwGo2LF6USCnbrsIhNngDfodhxbegfEOA== - dependencies: - "@emotion/is-prop-valid" "1.2.2" - "@emotion/unitless" "0.8.1" - "@types/stylis" "4.2.5" - css-to-react-native "3.2.0" - csstype "3.1.3" - postcss "8.4.38" - shallowequal "1.1.0" - stylis "4.3.2" - tslib "2.6.2" - styled-jsx@5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/styled-jsx/-/styled-jsx-5.1.1.tgz#839a1c3aaacc4e735fed0781b8619ea5d0009d1f" @@ -5765,34 +5901,23 @@ styled-jsx@5.1.1: dependencies: client-only "0.0.1" -styled-system@^5.1.5: - version "5.1.5" - resolved "https://registry.yarnpkg.com/styled-system/-/styled-system-5.1.5.tgz#e362d73e1dbb5641a2fd749a6eba1263dc85075e" - integrity sha512-7VoD0o2R3RKzOzPK0jYrVnS8iJdfkKsQJNiLRDjikOpQVqQHns/DXWaPZOH4tIKkhAT7I6wIsy9FWTWh2X3q+A== - dependencies: - "@styled-system/background" "^5.1.2" - "@styled-system/border" "^5.1.5" - "@styled-system/color" "^5.1.2" - "@styled-system/core" "^5.1.2" - "@styled-system/flexbox" "^5.1.2" - "@styled-system/grid" "^5.1.2" - "@styled-system/layout" "^5.1.2" - "@styled-system/position" "^5.1.2" - "@styled-system/shadow" "^5.1.2" - "@styled-system/space" "^5.1.2" - "@styled-system/typography" "^5.1.2" - "@styled-system/variant" "^5.1.5" - object-assign "^4.1.1" - stylis@4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.2.0.tgz#79daee0208964c8fe695a42fcffcac633a211a51" integrity sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw== -stylis@4.3.2: - version "4.3.2" - resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.3.2.tgz#8f76b70777dd53eb669c6f58c997bf0a9972e444" - integrity sha512-bhtUjWd/z6ltJiQwg0dUfxEJ+W+jdqQd8TbWLWyeIJHlnsqmGLRFFd8e5mA0AZi/zx90smXRlN66YMTcaSFifg== +sucrase@^3.32.0: + version "3.35.0" + resolved "https://registry.yarnpkg.com/sucrase/-/sucrase-3.35.0.tgz#57f17a3d7e19b36d8995f06679d121be914ae263" + integrity sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA== + dependencies: + "@jridgewell/gen-mapping" "^0.3.2" + commander "^4.0.0" + glob "^10.3.10" + lines-and-columns "^1.1.6" + mz "^2.7.0" + pirates "^4.0.1" + ts-interface-checker "^0.1.9" supports-color@^5.3.0: version "5.5.0" @@ -5851,6 +5976,55 @@ symbol-tree@^3.2.4: resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2" integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw== +tailwind-merge@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/tailwind-merge/-/tailwind-merge-2.3.0.tgz#27d2134fd00a1f77eca22bcaafdd67055917d286" + integrity sha512-vkYrLpIP+lgR0tQCG6AP7zZXCTLc1Lnv/CCRT3BqJ9CZ3ui2++GPaGb1x/ILsINIMSYqqvrpqjUFsMNLlW99EA== + dependencies: + "@babel/runtime" "^7.24.1" + +tailwindcss@^3.4.4: + version "3.4.4" + resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-3.4.4.tgz#351d932273e6abfa75ce7d226b5bf3a6cb257c05" + integrity sha512-ZoyXOdJjISB7/BcLTR6SEsLgKtDStYyYZVLsUtWChO4Ps20CBad7lfJKVDiejocV4ME1hLmyY0WJE3hSDcmQ2A== + dependencies: + "@alloc/quick-lru" "^5.2.0" + arg "^5.0.2" + chokidar "^3.5.3" + didyoumean "^1.2.2" + dlv "^1.1.3" + fast-glob "^3.3.0" + glob-parent "^6.0.2" + is-glob "^4.0.3" + jiti "^1.21.0" + lilconfig "^2.1.0" + micromatch "^4.0.5" + normalize-path "^3.0.0" + object-hash "^3.0.0" + picocolors "^1.0.0" + postcss "^8.4.23" + postcss-import "^15.1.0" + postcss-js "^4.0.1" + postcss-load-config "^4.0.1" + postcss-nested "^6.0.1" + postcss-selector-parser "^6.0.11" + resolve "^1.22.2" + sucrase "^3.32.0" + +thenify-all@^1.0.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/thenify-all/-/thenify-all-1.6.0.tgz#1a1918d402d8fc3f98fbf234db0bcc8cc10e9726" + integrity sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA== + dependencies: + thenify ">= 3.1.0 < 4" + +"thenify@>= 3.1.0 < 4": + version "3.3.1" + resolved "https://registry.yarnpkg.com/thenify/-/thenify-3.3.1.tgz#8932e686a4066038a016dd9e2ca46add9838a95f" + integrity sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw== + dependencies: + any-promise "^1.0.0" + throttleit@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/throttleit/-/throttleit-1.0.1.tgz#304ec51631c3b770c65c6c6f76938b384000f4d5" @@ -5876,6 +6050,13 @@ to-fast-properties@^2.0.0: resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" integrity sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog== +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + toposort@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/toposort/-/toposort-2.0.2.tgz#ae21768175d1559d48bef35420b2f4962f09c330" @@ -5908,10 +6089,10 @@ tr46@~0.0.3: resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== -tslib@2.6.2: - version "2.6.2" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" - integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== +ts-interface-checker@^0.1.9: + version "0.1.13" + resolved "https://registry.yarnpkg.com/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz#784fd3d679722bc103b1b4b8030bcddb5db2a699" + integrity sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA== tslib@^2.0.3, tslib@^2.1.0, tslib@^2.4.0: version "2.6.3" @@ -6116,6 +6297,11 @@ utf8@^2.1.0, utf8@^2.1.1: resolved "https://registry.yarnpkg.com/utf8/-/utf8-2.1.2.tgz#1fa0d9270e9be850d9b05027f63519bf46457d96" integrity sha512-QXo+O/QkLP/x1nyi54uQiG0XrODxdysuQvE5dtVqv7F5K2Qb6FsN+qbr6KhF5wQ20tfcV3VQp0/2x1e1MRSPWg== +util-deprecate@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== + uuencode@0.0.4: version "0.0.4" resolved "https://registry.yarnpkg.com/uuencode/-/uuencode-0.0.4.tgz#c8d50370885663879385ab37e333c7e8e3b0218c" @@ -6582,6 +6768,11 @@ yaml@^1.10.0: resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== +yaml@^2.3.4: + version "2.4.5" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.4.5.tgz#60630b206dd6d84df97003d33fc1ddf6296cca5e" + integrity sha512-aBx2bnqDzVOyNKfsysjA2ms5ZlnjSAW2eG3/L5G/CSujfjLJTJsEw1bGw8kCf04KodQWk1pxlGnZ56CRxiawmg== + yargs-parser@^21.1.1: version "21.1.1" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35"