From 278bcfc603268836d7edae1f2b02cbd3a31298f9 Mon Sep 17 00:00:00 2001 From: Paul Bienkowski Date: Sat, 26 Aug 2023 10:26:13 +0200 Subject: [PATCH] Format all JS/TS files --- frontend/src/App.tsx | 197 +++---- frontend/src/components/Avatar/index.tsx | 37 +- frontend/src/components/Chart.tsx | 23 +- frontend/src/components/ColorMapLegend.tsx | 43 +- frontend/src/components/FileUploadField.tsx | 56 +- frontend/src/components/FormattedDate.tsx | 6 +- frontend/src/components/Page/index.tsx | 22 +- frontend/src/components/Stats/index.tsx | 133 ++--- frontend/src/components/Visibility.tsx | 20 +- frontend/src/config.ts | 56 +- frontend/src/i18n.ts | 100 ++-- frontend/src/mapstyles/index.js | 235 ++++---- frontend/src/pages/AcknowledgementsPage.tsx | 18 +- frontend/src/pages/ExportPage/index.tsx | 221 ++++---- frontend/src/pages/LoginRedirectPage.tsx | 83 ++- frontend/src/pages/MapPage/LayerSidebar.tsx | 263 ++++----- frontend/src/pages/MapPage/index.tsx | 319 +++++------ frontend/src/pages/MyTracksPage.tsx | 341 +++++------- frontend/src/pages/NotFoundPage.js | 4 +- .../src/pages/SettingsPage/ApiKeySettings.tsx | 131 ++--- .../src/pages/SettingsPage/DeviceList.tsx | 2 +- .../pages/SettingsPage/UserSettingsForm.tsx | 78 ++- frontend/src/pages/SettingsPage/index.tsx | 118 ++-- frontend/src/pages/TrackEditor.tsx | 311 +++++------ frontend/src/pages/TrackPage/TrackActions.tsx | 10 +- .../src/pages/TrackPage/TrackComments.tsx | 73 +-- frontend/src/pages/TrackPage/TrackDetails.tsx | 72 +-- frontend/src/pages/TrackPage/index.tsx | 511 ++++++++---------- frontend/src/pages/TracksPage.tsx | 165 +++--- frontend/src/pages/UploadPage.tsx | 174 +++--- frontend/src/pages/index.js | 26 +- frontend/src/query.ts | 2 +- frontend/src/types.ts | 83 ++- frontend/src/utils.js | 38 +- frontend/webpack.config.js | 12 +- 35 files changed, 1717 insertions(+), 2266 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 5ba7c29..8c79f14 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,24 +1,17 @@ -import React from "react"; -import classnames from "classnames"; -import { connect } from "react-redux"; -import { - List, - Grid, - Container, - Menu, - Header, - Dropdown, -} from "semantic-ui-react"; -import { BrowserRouter as Router, Switch, Route, Link } from "react-router-dom"; -import { useObservable } from "rxjs-hooks"; -import { from } from "rxjs"; -import { pluck } from "rxjs/operators"; -import { Helmet } from "react-helmet"; -import { useTranslation } from "react-i18next"; +import React from 'react' +import classnames from 'classnames' +import {connect} from 'react-redux' +import {List, Grid, Container, Menu, Header, Dropdown} from 'semantic-ui-react' +import {BrowserRouter as Router, Switch, Route, Link} from 'react-router-dom' +import {useObservable} from 'rxjs-hooks' +import {from} from 'rxjs' +import {pluck} from 'rxjs/operators' +import {Helmet} from 'react-helmet' +import {useTranslation} from 'react-i18next' -import { useConfig } from "config"; -import styles from "./App.module.less"; -import { AVAILABLE_LOCALES, setLocale } from "i18n"; +import {useConfig} from 'config' +import styles from './App.module.less' +import {AVAILABLE_LOCALES, setLocale} from 'i18n' import { AcknowledgementsPage, @@ -34,60 +27,50 @@ import { TracksPage, UploadPage, MyTracksPage, -} from "pages"; -import { Avatar, LoginButton } from "components"; -import api from "api"; +} from 'pages' +import {Avatar, LoginButton} from 'components' +import api from 'api' // This component removes the "navigate" prop before rendering a Menu.Item, // which is a workaround for an annoying warning that is somehow caused by the // and combination. -function MenuItemForLink({ navigate, ...props }) { +function MenuItemForLink({navigate, ...props}) { return ( { - e.preventDefault(); - navigate(); + e.preventDefault() + navigate() }} /> - ); + ) } -function DropdownItemForLink({ navigate, ...props }) { +function DropdownItemForLink({navigate, ...props}) { return ( { - e.preventDefault(); - navigate(); + e.preventDefault() + navigate() }} /> - ); + ) } -function Banner({ - text, - style = "warning", -}: { - text: string; - style: "warning" | "info"; -}) { - return
{text}
; +function Banner({text, style = 'warning'}: {text: string; style: 'warning' | 'info'}) { + return
{text}
} -const App = connect((state) => ({ login: state.login }))(function App({ - login, -}) { - const { t } = useTranslation(); - const config = useConfig(); - const apiVersion = useObservable(() => - from(api.get("/info")).pipe(pluck("version")) - ); +const App = connect((state) => ({login: state.login}))(function App({login}) { + const {t} = useTranslation() + const config = useConfig() + const apiVersion = useObservable(() => from(api.get('/info')).pipe(pluck('version'))) - const hasMap = Boolean(config?.obsMapSource); + const hasMap = Boolean(config?.obsMapSource) React.useEffect(() => { - api.loadUser(); - }, []); + api.loadUser() + }, []) return config ? ( @@ -98,59 +81,41 @@ const App = connect((state) => ({ login: state.login }))(function App({ {config?.banner && } - + OpenBikeSensor {hasMap && ( - {t("App.menu.map")} + {t('App.menu.map')} )} - {t("App.menu.tracks")} + {t('App.menu.tracks')} - {t("App.menu.export")} + {t('App.menu.export')} {login ? ( <> - {t("App.menu.myTracks")} + {t('App.menu.myTracks')} - } - > + }> - + - + @@ -216,14 +181,10 @@ const App = connect((state) => ({ login: state.login }))(function App({ -
{t("App.footer.aboutTheProject")}
+
{t('App.footer.aboutTheProject')}
- + openbikesensor.org @@ -231,94 +192,66 @@ const App = connect((state) => ({ login: state.login }))(function App({
-
{t("App.footer.getInvolved")}
+
{t('App.footer.getInvolved')}
- - {t("App.footer.getHelpInForum")} + + {t('App.footer.getHelpInForum')} - - {t("App.footer.reportAnIssue")} + + {t('App.footer.reportAnIssue')} - - {t("App.footer.development")} + + {t('App.footer.development')}
-
{t("App.footer.thisInstallation")}
+
{t('App.footer.thisInstallation')}
- - {t("App.footer.privacyPolicy")} + + {t('App.footer.privacyPolicy')} - - {t("App.footer.imprint")} + + {t('App.footer.imprint')} {config?.termsUrl && ( - - {t("App.footer.terms")} + + {t('App.footer.terms')} )} - {apiVersion - ? t("App.footer.version", { apiVersion }) - : t("App.footer.versionLoading")} + {apiVersion ? t('App.footer.version', {apiVersion}) : t('App.footer.versionLoading')}
-
{t("App.footer.changeLanguage")}
+
{t('App.footer.changeLanguage')}
{AVAILABLE_LOCALES.map((locale) => ( - setLocale(locale)}> - {t(`locales.${locale}`)} - + setLocale(locale)}>{t(`locales.${locale}`)} ))} @@ -328,7 +261,7 @@ const App = connect((state) => ({ login: state.login }))(function App({
- ) : null; -}); + ) : null +}) -export default App; +export default App diff --git a/frontend/src/components/Avatar/index.tsx b/frontend/src/components/Avatar/index.tsx index 72f85eb..8382ec0 100644 --- a/frontend/src/components/Avatar/index.tsx +++ b/frontend/src/components/Avatar/index.tsx @@ -1,42 +1,39 @@ -import React from "react"; -import { Comment } from "semantic-ui-react"; -import classnames from "classnames"; +import React from 'react' +import {Comment} from 'semantic-ui-react' +import classnames from 'classnames' -import "./styles.less"; +import './styles.less' function hashCode(s) { - let hash = 0; + let hash = 0 for (let i = 0; i < s.length; i++) { - hash = (hash << 5) - hash + s.charCodeAt(i); - hash |= 0; + hash = (hash << 5) - hash + s.charCodeAt(i) + hash |= 0 } - return hash; + return hash } function getColor(s) { - const h = Math.floor(hashCode(s)) % 360; - return `hsl(${h}, 50%, 50%)`; + const h = Math.floor(hashCode(s)) % 360 + return `hsl(${h}, 50%, 50%)` } -export default function Avatar({ user, className }) { - const { image, displayName } = user || {}; +export default function Avatar({user, className}) { + const {image, displayName} = user || {} if (image) { - return ; + return } if (!displayName) { - return
; + return
} - const color = getColor(displayName); + const color = getColor(displayName) return ( -
+
{displayName && {displayName[0]}}
- ); + ) } diff --git a/frontend/src/components/Chart.tsx b/frontend/src/components/Chart.tsx index c021323..60992fa 100644 --- a/frontend/src/components/Chart.tsx +++ b/frontend/src/components/Chart.tsx @@ -1,7 +1,7 @@ -import React from 'react'; -import ReactEChartsCore from 'echarts-for-react/lib/core'; +import React from 'react' +import ReactEChartsCore from 'echarts-for-react/lib/core' -import * as echarts from 'echarts/core'; +import * as echarts from 'echarts/core' import { // LineChart, @@ -26,7 +26,7 @@ import { // ThemeRiverChart, // SunburstChart, // CustomChart, -} from 'echarts/charts'; +} from 'echarts/charts' // import components, all suffixed with Component import { @@ -60,25 +60,18 @@ import { // AriaComponent, // TransformComponent, DatasetComponent, -} from 'echarts/components'; +} from 'echarts/components' // Import renderer, note that introducing the CanvasRenderer or SVGRenderer is a required step import { CanvasRenderer, // SVGRenderer, -} from 'echarts/renderers'; +} from 'echarts/renderers' // Register the required components -echarts.use( - [TitleComponent, TooltipComponent, GridComponent, BarChart, CanvasRenderer] -); +echarts.use([TitleComponent, TooltipComponent, GridComponent, BarChart, CanvasRenderer]) // The usage of ReactEChartsCore are same with above. export default function Chart(props) { - return + return } diff --git a/frontend/src/components/ColorMapLegend.tsx b/frontend/src/components/ColorMapLegend.tsx index ca09860..e28c293 100644 --- a/frontend/src/components/ColorMapLegend.tsx +++ b/frontend/src/components/ColorMapLegend.tsx @@ -1,18 +1,18 @@ -import React, {useMemo} from "react"; - -type ColorMap = [number, string][] +import React, {useMemo} from 'react' import styles from './ColorMapLegend.module.less' +type ColorMap = [number, string][] + function* pairs(arr) { for (let i = 1; i < arr.length; i++) { - yield [arr[i - 1], arr[i]]; + yield [arr[i - 1], arr[i]] } } function* zip(...arrs) { - const l = Math.min(...arrs.map(a => a.length)); + const l = Math.min(...arrs.map((a) => a.length)) for (let i = 0; i < l; i++) { - yield arrs.map(a => a[i]); + yield arrs.map((a) => a[i]) } } @@ -25,10 +25,10 @@ export function DiscreteColorMapLegend({map}: {map: ColorMap}) { min -= buffer max += buffer const normalizeValue = (v) => (v - min) / (max - min) - const stopPairs = Array.from(pairs([min, ...stops, max])); + const stopPairs = Array.from(pairs([min, ...stops, max])) - const gradientId = useMemo(() => `gradient${Math.floor(Math.random() * 1000000)}`, []); - const gradientUrl = `url(#${gradientId})`; + const gradientId = useMemo(() => `gradient${Math.floor(Math.random() * 1000000)}`, []) + const gradientUrl = `url(#${gradientId})` const parts = Array.from(zip(stopPairs, colors)) @@ -38,11 +38,10 @@ export function DiscreteColorMapLegend({map}: {map: ColorMap}) { {parts.map(([[left, right], color]) => ( - - - - - + + + + ))} @@ -59,13 +58,21 @@ export function DiscreteColorMapLegend({map}: {map: ColorMap}) { ) } -export default function ColorMapLegend({map, twoTicks = false, digits=2}: {map: ColorMap, twoTicks?: boolean, digits?: number}) { +export default function ColorMapLegend({ + map, + twoTicks = false, + digits = 2, +}: { + map: ColorMap + twoTicks?: boolean + digits?: number +}) { const min = map[0][0] const max = map[map.length - 1][0] const normalizeValue = (v) => (v - min) / (max - min) - const gradientId = useMemo(() => `gradient${Math.floor(Math.random() * 1000000)}`, []); - const gradientUrl = `url(#${gradientId})`; - const tickValues = twoTicks ? [map[0], map[map.length-1]] : map + const gradientId = useMemo(() => `gradient${Math.floor(Math.random() * 1000000)}`, []) + const gradientUrl = `url(#${gradientId})` + const tickValues = twoTicks ? [map[0], map[map.length - 1]] : map return (
diff --git a/frontend/src/components/FileUploadField.tsx b/frontend/src/components/FileUploadField.tsx index 4768764..0c827a5 100644 --- a/frontend/src/components/FileUploadField.tsx +++ b/frontend/src/components/FileUploadField.tsx @@ -1,31 +1,31 @@ -import React from "react"; -import { Icon, Segment, Header, Button } from "semantic-ui-react"; -import { useTranslation } from "react-i18next"; +import React from 'react' +import {Icon, Segment, Header, Button} from 'semantic-ui-react' +import {useTranslation} from 'react-i18next' -import { FileDrop } from "components"; +import {FileDrop} from 'components' -export default function FileUploadField({ onSelect: onSelect_, multiple }) { - const { t } = useTranslation(); - const labelRef = React.useRef(); - const [labelRefState, setLabelRefState] = React.useState(); +export default function FileUploadField({onSelect: onSelect_, multiple}) { + const {t} = useTranslation() + const labelRef = React.useRef() + const [labelRefState, setLabelRefState] = React.useState() - const onSelect = multiple ? onSelect_ : (files) => onSelect_(files?.[0]); + const onSelect = multiple ? onSelect_ : (files) => onSelect_(files?.[0]) React.useLayoutEffect( () => { - setLabelRefState(labelRef.current); + setLabelRefState(labelRef.current) }, // eslint-disable-next-line react-hooks/exhaustive-deps [labelRef.current] - ); + ) function onChangeField(e) { - e.preventDefault?.(); + e.preventDefault?.() if (e.target.files && e.target.files.length) { - onSelect(e.target.files); + onSelect(e.target.files) } - e.target.value = ""; // reset the form field for uploading again + e.target.value = '' // reset the form field for uploading again } return ( @@ -36,7 +36,7 @@ export default function FileUploadField({ onSelect: onSelect_, multiple }) { style={{ width: 0, height: 0, - position: "fixed", + position: 'fixed', left: -1000, top: -1000, opacity: 0.001, @@ -48,34 +48,22 @@ export default function FileUploadField({ onSelect: onSelect_, multiple }) { - ); + ) } diff --git a/frontend/src/components/FormattedDate.tsx b/frontend/src/components/FormattedDate.tsx index 49ae9d0..032d647 100644 --- a/frontend/src/components/FormattedDate.tsx +++ b/frontend/src/components/FormattedDate.tsx @@ -21,5 +21,9 @@ export default function FormattedDate({date, relative = false}) { } const iso = dateTime.toISO() - return + return ( + + ) } diff --git a/frontend/src/components/Page/index.tsx b/frontend/src/components/Page/index.tsx index c881f14..c7614a0 100644 --- a/frontend/src/components/Page/index.tsx +++ b/frontend/src/components/Page/index.tsx @@ -1,9 +1,9 @@ -import React from "react"; -import classnames from "classnames"; -import { Container } from "semantic-ui-react"; -import { Helmet } from "react-helmet"; +import React from 'react' +import classnames from 'classnames' +import {Container} from 'semantic-ui-react' +import {Helmet} from 'react-helmet' -import styles from "./Page.module.less"; +import styles from './Page.module.less' export default function Page({ small, @@ -12,11 +12,11 @@ export default function Page({ stage, title, }: { - small?: boolean; - children: ReactNode; - fullScreen?: boolean; - stage?: ReactNode; - title?: string; + small?: boolean + children: ReactNode + fullScreen?: boolean + stage?: ReactNode + title?: string }) { return ( <> @@ -37,5 +37,5 @@ export default function Page({ {fullScreen ? children : {children}} - ); + ) } diff --git a/frontend/src/components/Stats/index.tsx b/frontend/src/components/Stats/index.tsx index b558930..5e03596 100644 --- a/frontend/src/components/Stats/index.tsx +++ b/frontend/src/components/Stats/index.tsx @@ -1,76 +1,71 @@ -import React, { useState, useCallback } from "react"; -import { pickBy } from "lodash"; -import { Loader, Statistic, Segment, Header, Menu } from "semantic-ui-react"; -import { useObservable } from "rxjs-hooks"; -import { of, from, concat, combineLatest } from "rxjs"; -import { map, switchMap, distinctUntilChanged } from "rxjs/operators"; -import { Duration, DateTime } from "luxon"; -import { useTranslation } from "react-i18next"; +import React, {useState, useCallback} from 'react' +import {pickBy} from 'lodash' +import {Loader, Statistic, Segment, Header, Menu} from 'semantic-ui-react' +import {useObservable} from 'rxjs-hooks' +import {of, from, concat, combineLatest} from 'rxjs' +import {map, switchMap, distinctUntilChanged} from 'rxjs/operators' +import {Duration, DateTime} from 'luxon' +import {useTranslation} from 'react-i18next' -import api from "api"; +import api from 'api' function formatDuration(seconds) { return ( Duration.fromMillis((seconds ?? 0) * 1000) - .as("hours") - .toFixed(1) + " h" - ); + .as('hours') + .toFixed(1) + ' h' + ) } -export default function Stats({ user = null }: { user?: null | string }) { - const { t } = useTranslation(); - const [timeframe, setTimeframe] = useState("all_time"); - const onClick = useCallback( - (_e, { name }) => setTimeframe(name), - [setTimeframe] - ); +export default function Stats({user = null}: {user?: null | string}) { + const {t} = useTranslation() + const [timeframe, setTimeframe] = useState('all_time') + const onClick = useCallback((_e, {name}) => setTimeframe(name), [setTimeframe]) const stats = useObservable( (_$, inputs$) => { const timeframe$ = inputs$.pipe( map((inputs) => inputs[0]), distinctUntilChanged() - ); + ) const user$ = inputs$.pipe( map((inputs) => inputs[1]), distinctUntilChanged() - ); + ) return combineLatest(timeframe$, user$).pipe( map(([timeframe_, user_]) => { - const now = DateTime.now(); + const now = DateTime.now() - let start, end; + let start, end switch (timeframe_) { - case "this_month": - start = now.startOf("month"); - end = now.endOf("month"); - break; + case 'this_month': + start = now.startOf('month') + end = now.endOf('month') + break - case "this_year": - start = now.startOf("year"); - end = now.endOf("year"); - break; + case 'this_year': + start = now.startOf('year') + end = now.endOf('year') + break } return pickBy({ start: start?.toISODate(), end: end?.toISODate(), user: user_, - }); + }) }), - switchMap((query) => - concat(of(null), from(api.get("/stats", { query }))) - ) - ); + switchMap((query) => concat(of(null), from(api.get('/stats', {query})))) + ) }, null, [timeframe, user] - ); + ) - const placeholder = t("Stats.placeholder"); + const placeholder = t('Stats.placeholder') return ( <> @@ -80,43 +75,31 @@ export default function Stats({ user = null }: { user?: null | string }) { - {stats - ? `${Number(stats?.trackLength / 1000).toFixed(1)} km` - : placeholder} + {stats ? `${Number(stats?.trackLength / 1000).toFixed(1)} km` : placeholder} - {t("Stats.totalTrackLength")} + {t('Stats.totalTrackLength')} - - {stats ? formatDuration(stats?.trackDuration) : placeholder} - - {t("Stats.timeRecorded")} + {stats ? formatDuration(stats?.trackDuration) : placeholder} + {t('Stats.timeRecorded')} - - {stats?.numEvents ?? placeholder} - - {t("Stats.eventsConfirmed")} + {stats?.numEvents ?? placeholder} + {t('Stats.eventsConfirmed')} - - {stats?.trackCount ?? placeholder} - - {t("Stats.tracksRecorded")} + {stats?.trackCount ?? placeholder} + {t('Stats.tracksRecorded')} {!user && ( <> - - {stats?.userCount ?? placeholder} - - {t("Stats.membersJoined")} + {stats?.userCount ?? placeholder} + {t('Stats.membersJoined')} - - {stats?.deviceCount ?? placeholder} - - {t("Stats.deviceCount")} + {stats?.deviceCount ?? placeholder} + {t('Stats.deviceCount')} )} @@ -124,29 +107,17 @@ export default function Stats({ user = null }: { user?: null | string }) { - - {t("Stats.thisMonth")} + + {t('Stats.thisMonth')} - - {t("Stats.thisYear")} + + {t('Stats.thisYear')} - - {t("Stats.allTime")} + + {t('Stats.allTime')}
- ); + ) } diff --git a/frontend/src/components/Visibility.tsx b/frontend/src/components/Visibility.tsx index 453627a..78a29b5 100644 --- a/frontend/src/components/Visibility.tsx +++ b/frontend/src/components/Visibility.tsx @@ -1,18 +1,14 @@ -import React from "react"; -import { Icon } from "semantic-ui-react"; -import { useTranslation } from "react-i18next"; +import React from 'react' +import {Icon} from 'semantic-ui-react' +import {useTranslation} from 'react-i18next' -export default function Visibility({ public: public_ }: { public: boolean }) { - const { t } = useTranslation(); - const icon = public_ ? ( - - ) : ( - - ); - const text = public_ ? t("general.public") : t("general.private"); +export default function Visibility({public: public_}: {public: boolean}) { + const {t} = useTranslation() + const icon = public_ ? : + const text = public_ ? t('general.public') : t('general.private') return ( <> {icon} {text} - ); + ) } diff --git a/frontend/src/config.ts b/frontend/src/config.ts index 588331f..9e20772 100644 --- a/frontend/src/config.ts +++ b/frontend/src/config.ts @@ -1,46 +1,46 @@ -import React from "react"; +import React from 'react' export type MapSource = { - type: "vector"; - tiles: string[]; - minzoom: number; - maxzoom: number; -}; + type: 'vector' + tiles: string[] + minzoom: number + maxzoom: number +} export interface Config { - apiUrl: string; + apiUrl: string mapHome: { - latitude: number; - longitude: number; - zoom: number; - }; - obsMapSource?: MapSource; - imprintUrl?: string; - privacyPolicyUrl?: string; - termsUrl?: string; + latitude: number + longitude: number + zoom: number + } + obsMapSource?: MapSource + imprintUrl?: string + privacyPolicyUrl?: string + termsUrl?: string banner?: { - text: string; - style?: "warning" | "info"; - }; + text: string + style?: 'warning' | 'info' + } } async function loadConfig(): Promise { - const response = await fetch(__webpack_public_path__ + "config.json"); - const config = await response.json(); - return config; + const response = await fetch(__webpack_public_path__ + 'config.json') + const config = await response.json() + return config } -let _configPromise: Promise = loadConfig(); -let _configCache: null | Config = null; +let _configPromise: Promise = loadConfig() +let _configCache: null | Config = null export function useConfig() { - const [config, setConfig] = React.useState(_configCache); + const [config, setConfig] = React.useState(_configCache) React.useEffect(() => { if (!_configCache) { - _configPromise.then(setConfig); + _configPromise.then(setConfig) } - }, []); - return config; + }, []) + return config } -export default _configPromise; +export default _configPromise diff --git a/frontend/src/i18n.ts b/frontend/src/i18n.ts index ab991e3..f0e860c 100644 --- a/frontend/src/i18n.ts +++ b/frontend/src/i18n.ts @@ -1,95 +1,87 @@ -import { useState, useEffect, useMemo } from "react"; -import i18next, { TOptions } from "i18next"; -import { BehaviorSubject, combineLatest } from "rxjs"; -import { map, distinctUntilChanged } from "rxjs/operators"; -import HttpBackend, { - BackendOptions, - RequestCallback, -} from "i18next-http-backend"; -import { initReactI18next } from "react-i18next"; -import LanguageDetector from "i18next-browser-languagedetector"; +import {useState, useEffect, useMemo} from 'react' +import i18next, {TOptions} from 'i18next' +import {BehaviorSubject, combineLatest} from 'rxjs' +import {map, distinctUntilChanged} from 'rxjs/operators' +import HttpBackend, {BackendOptions, RequestCallback} from 'i18next-http-backend' +import {initReactI18next} from 'react-i18next' +import LanguageDetector from 'i18next-browser-languagedetector' -export type AvailableLocales = "en" | "de" | "fr"; +export type AvailableLocales = 'en' | 'de' | 'fr' -async function request( - _options: BackendOptions, - url: string, - _payload: any, - callback: RequestCallback -) { +async function request(_options: BackendOptions, url: string, _payload: any, callback: RequestCallback) { try { - const [lng] = url.split("/"); - const locale = await import(`translations/${lng}.yaml`); - callback(null, { status: 200, data: locale }); + const [lng] = url.split('/') + const locale = await import(`translations/${lng}.yaml`) + callback(null, {status: 200, data: locale}) } catch (e) { - console.error(`Unable to load locale at ${url}\n`, e); - callback(null, { status: 404, data: String(e) }); + console.error(`Unable to load locale at ${url}\n`, e) + callback(null, {status: 404, data: String(e)}) } } -export const AVAILABLE_LOCALES: AvailableLocales[] = ["en", "de", "fr"]; +export const AVAILABLE_LOCALES: AvailableLocales[] = ['en', 'de', 'fr'] -const i18n = i18next.createInstance(); +const i18n = i18next.createInstance() const options: TOptions = { - fallbackLng: "en", + fallbackLng: 'en', - ns: ["common"], - defaultNS: "common", + ns: ['common'], + defaultNS: 'common', whitelist: AVAILABLE_LOCALES, // loading via webpack backend: { - loadPath: "{{lng}}/{{ns}}", + loadPath: '{{lng}}/{{ns}}', parse: (data: any) => data, request, }, - load: "languageOnly", + load: 'languageOnly', interpolation: { escapeValue: false, // not needed for react as it escapes by default }, -}; +} i18n .use(HttpBackend) .use(initReactI18next) .use(LanguageDetector) - .init({ ...options }); + .init({...options}) -const locale$ = new BehaviorSubject("en"); +const locale$ = new BehaviorSubject('en') -export const translate = i18n.t.bind(i18n); +export const translate = i18n.t.bind(i18n) export const translate$ = (stringAndData$: [string, any]) => combineLatest([stringAndData$, locale$.pipe(distinctUntilChanged())]).pipe( map(([stringAndData]) => { - if (typeof stringAndData === "string") { - return i18n.t(stringAndData); + if (typeof stringAndData === 'string') { + return i18n.t(stringAndData) } else { - const [string, data] = stringAndData; - return i18n.t(string, { data }); + const [string, data] = stringAndData + return i18n.t(string, {data}) } }) - ); + ) export const setLocale = (locale: AvailableLocales) => { - i18n.changeLanguage(locale); - locale$.next(locale); -}; - -export function useLocale() { - const [, reload] = useState(); - - useEffect(() => { - i18n.on("languageChanged", reload); - return () => { - i18n.off("languageChanged", reload); - }; - }, []); - - return i18n.language; + i18n.changeLanguage(locale) + locale$.next(locale) } -export default i18n; +export function useLocale() { + const [, reload] = useState() + + useEffect(() => { + i18n.on('languageChanged', reload) + return () => { + i18n.off('languageChanged', reload) + } + }, []) + + return i18n.language +} + +export default i18n diff --git a/frontend/src/mapstyles/index.js b/frontend/src/mapstyles/index.js index e484359..dce43e7 100644 --- a/frontend/src/mapstyles/index.js +++ b/frontend/src/mapstyles/index.js @@ -1,169 +1,146 @@ -import _ from "lodash"; -import produce from "immer"; +import _ from 'lodash' +import produce from 'immer' -import bright from "./bright.json"; -import positron from "./positron.json"; +import bright from './bright.json' +import positron from './positron.json' -import viridisBase from "colormap/res/res/viridis"; +import viridisBase from 'colormap/res/res/viridis' -export { bright, positron }; -export const baseMapStyles = { bright, positron }; +export {bright, positron} +export const baseMapStyles = {bright, positron} function simplifyColormap(colormap, maxCount = 16) { - const result = []; - const step = Math.ceil(colormap.length / maxCount); + const result = [] + const step = Math.ceil(colormap.length / maxCount) for (let i = 0; i < colormap.length; i += step) { - result.push(colormap[i]); + result.push(colormap[i]) } - return result; + return result } function rgbArrayToColor(arr) { - return ["rgb", ...arr.map((v) => Math.round(v * 255))]; + return ['rgb', ...arr.map((v) => Math.round(v * 255))] } function rgbArrayToHtml(arr) { return ( - "#" + + '#' + arr .map((v) => Math.round(v * 255).toString(16)) - .map((v) => (v.length == 1 ? "0" : "") + v) - .join("") - ); + .map((v) => (v.length == 1 ? '0' : '') + v) + .join('') + ) } export function colormapToScale(colormap, value, min, max) { return [ - "interpolate-hcl", - ["linear"], + 'interpolate-hcl', + ['linear'], value, - ...colormap.flatMap((v, i, a) => [ - (i / (a.length - 1)) * (max - min) + min, - v, - ]), - ]; + ...colormap.flatMap((v, i, a) => [(i / (a.length - 1)) * (max - min) + min, v]), + ] } -export const viridis = simplifyColormap(viridisBase.map(rgbArrayToColor), 20); -export const viridisSimpleHtml = simplifyColormap( - viridisBase.map(rgbArrayToHtml), - 10 -); -export const grayscale = ["#FFFFFF", "#000000"]; -export const reds = ["rgba( 255, 0, 0, 0)", "rgba( 255, 0, 0, 255)"]; +export const viridis = simplifyColormap(viridisBase.map(rgbArrayToColor), 20) +export const viridisSimpleHtml = simplifyColormap(viridisBase.map(rgbArrayToHtml), 10) +export const grayscale = ['#FFFFFF', '#000000'] +export const reds = ['rgba( 255, 0, 0, 0)', 'rgba( 255, 0, 0, 255)'] -export function colorByCount( - attribute = "event_count", - maxCount, - colormap = viridis -) { - return colormapToScale( - colormap, - ["case", isValidAttribute(attribute), ["get", attribute], 0], - 0, - maxCount - ); +export function colorByCount(attribute = 'event_count', maxCount, colormap = viridis) { + return colormapToScale(colormap, ['case', isValidAttribute(attribute), ['get', attribute], 0], 0, maxCount) } -var steps = { rural: [1.6, 1.8, 2.0, 2.2], urban: [1.1, 1.3, 1.5, 1.7] }; +var steps = {rural: [1.6, 1.8, 2.0, 2.2], urban: [1.1, 1.3, 1.5, 1.7]} export function isValidAttribute(attribute) { - if (attribute.endsWith("zone")) { - return ["in", ["get", attribute], ["literal", ["rural", "urban"]]]; + if (attribute.endsWith('zone')) { + return ['in', ['get', attribute], ['literal', ['rural', 'urban']]] } - return ["to-boolean", ["get", attribute]]; + return ['to-boolean', ['get', attribute]] } export function borderByZone() { - return ["match", ["get", "zone"], "rural", "cyan", "urban", "blue", "purple"]; + return ['match', ['get', 'zone'], 'rural', 'cyan', 'urban', 'blue', 'purple'] } -export function colorByDistance( - attribute = "distance_overtaker_mean", - fallback = "#ABC", - zone = "urban" -) { +export function colorByDistance(attribute = 'distance_overtaker_mean', fallback = '#ABC', zone = 'urban') { return [ - "case", - ["!", isValidAttribute(attribute)], + 'case', + ['!', isValidAttribute(attribute)], fallback, [ - "match", - ["get", "zone"], - "rural", + 'match', + ['get', 'zone'], + 'rural', [ - "step", - ["get", attribute], - "rgba(150, 0, 0, 1)", - steps["rural"][0], - "rgba(255, 0, 0, 1)", - steps["rural"][1], - "rgba(255, 220, 0, 1)", - steps["rural"][2], - "rgba(67, 200, 0, 1)", - steps["rural"][3], - "rgba(67, 150, 0, 1)", + 'step', + ['get', attribute], + 'rgba(150, 0, 0, 1)', + steps['rural'][0], + 'rgba(255, 0, 0, 1)', + steps['rural'][1], + 'rgba(255, 220, 0, 1)', + steps['rural'][2], + 'rgba(67, 200, 0, 1)', + steps['rural'][3], + 'rgba(67, 150, 0, 1)', ], - "urban", + 'urban', [ - "step", - ["get", attribute], - "rgba(150, 0, 0, 1)", - steps["urban"][0], - "rgba(255, 0, 0, 1)", - steps["urban"][1], - "rgba(255, 220, 0, 1)", - steps["urban"][2], - "rgba(67, 200, 0, 1)", - steps["urban"][3], - "rgba(67, 150, 0, 1)", + 'step', + ['get', attribute], + 'rgba(150, 0, 0, 1)', + steps['urban'][0], + 'rgba(255, 0, 0, 1)', + steps['urban'][1], + 'rgba(255, 220, 0, 1)', + steps['urban'][2], + 'rgba(67, 200, 0, 1)', + steps['urban'][3], + 'rgba(67, 150, 0, 1)', ], [ - "step", - ["get", attribute], - "rgba(150, 0, 0, 1)", - steps["urban"][0], - "rgba(255, 0, 0, 1)", - steps["urban"][1], - "rgba(255, 220, 0, 1)", - steps["urban"][2], - "rgba(67, 200, 0, 1)", - steps["urban"][3], - "rgba(67, 150, 0, 1)", + 'step', + ['get', attribute], + 'rgba(150, 0, 0, 1)', + steps['urban'][0], + 'rgba(255, 0, 0, 1)', + steps['urban'][1], + 'rgba(255, 220, 0, 1)', + steps['urban'][2], + 'rgba(67, 200, 0, 1)', + steps['urban'][3], + 'rgba(67, 150, 0, 1)', ], ], - ]; + ] } export const trackLayer = { - type: "line", + type: 'line', paint: { - "line-width": ["interpolate", ["linear"], ["zoom"], 14, 2, 17, 5], - "line-color": "#F06292", - "line-opacity": 0.6, + 'line-width': ['interpolate', ['linear'], ['zoom'], 14, 2, 17, 5], + 'line-color': '#F06292', + 'line-opacity': 0.6, }, -}; +} -export const getRegionLayers = ( - adminLevel = 6, - baseColor = "#00897B", - maxValue = 5000 -) => [ +export const getRegionLayers = (adminLevel = 6, baseColor = '#00897B', maxValue = 5000) => [ { - id: "region", - type: "fill", - source: "obs", - "source-layer": "obs_regions", + id: 'region', + type: 'fill', + source: 'obs', + 'source-layer': 'obs_regions', minzoom: 0, maxzoom: 10, // filter: [">", "overtaking_event_count", 0], paint: { - "fill-color": baseColor, - "fill-antialias": true, - "fill-opacity": [ - "interpolate", - ["linear"], - ["log10", ["max",["get", "overtaking_event_count"],1]], + 'fill-color': baseColor, + 'fill-antialias': true, + 'fill-opacity': [ + 'interpolate', + ['linear'], + ['log10', ['max', ['get', 'overtaking_event_count'], 1]], 0, 0, Math.log10(maxValue), @@ -172,38 +149,38 @@ export const getRegionLayers = ( }, }, { - id: "region-border", - type: "line", - source: "obs", - "source-layer": "obs_regions", + id: 'region-border', + type: 'line', + source: 'obs', + 'source-layer': 'obs_regions', minzoom: 0, maxzoom: 10, // filter: [">", "overtaking_event_count", 0], paint: { - "line-width": [ - "interpolate", - ["linear"], - ["log10", ["max",["get", "overtaking_event_count"],1]], + 'line-width': [ + 'interpolate', + ['linear'], + ['log10', ['max', ['get', 'overtaking_event_count'], 1]], 0, 0.2, Math.log10(maxValue), 1.5, ], - "line-color": baseColor, + 'line-color': baseColor, }, layout: { - "line-join": "round", - "line-cap": "round", + 'line-join': 'round', + 'line-cap': 'round', }, }, -]; +] export const trackLayerRaw = produce(trackLayer, (draft) => { // draft.paint['line-color'] = '#81D4FA' - draft.paint["line-width"][4] = 1; - draft.paint["line-width"][6] = 2; - draft.paint["line-dasharray"] = [3, 3]; - delete draft.paint["line-opacity"]; -}); + draft.paint['line-width'][4] = 1 + draft.paint['line-width'][6] = 2 + draft.paint['line-dasharray'] = [3, 3] + delete draft.paint['line-opacity'] +}) -export const basemap = positron; +export const basemap = positron diff --git a/frontend/src/pages/AcknowledgementsPage.tsx b/frontend/src/pages/AcknowledgementsPage.tsx index cc9cb19..f532a8b 100644 --- a/frontend/src/pages/AcknowledgementsPage.tsx +++ b/frontend/src/pages/AcknowledgementsPage.tsx @@ -1,18 +1,18 @@ -import React from "react"; -import { Header } from "semantic-ui-react"; -import { useTranslation } from "react-i18next"; -import Markdown from "react-markdown"; +import React from 'react' +import {Header} from 'semantic-ui-react' +import {useTranslation} from 'react-i18next' +import Markdown from 'react-markdown' -import { Page } from "components"; +import {Page} from 'components' export default function AcknowledgementsPage() { - const { t } = useTranslation(); - const title = t("AcknowledgementsPage.title"); + const {t} = useTranslation() + const title = t('AcknowledgementsPage.title') return (
{title}
- {t("AcknowledgementsPage.information")} + {t('AcknowledgementsPage.information')}
- ); + ) } diff --git a/frontend/src/pages/ExportPage/index.tsx b/frontend/src/pages/ExportPage/index.tsx index 65cdac5..3d82721 100644 --- a/frontend/src/pages/ExportPage/index.tsx +++ b/frontend/src/pages/ExportPage/index.tsx @@ -1,134 +1,121 @@ -import React, { useState, useCallback, useMemo } from "react"; -import { Source, Layer } from "react-map-gl"; -import _ from "lodash"; -import { - Button, - Form, - Dropdown, - Header, - Message, - Icon, -} from "semantic-ui-react"; -import { useTranslation, Trans as Translate } from "react-i18next"; -import Markdown from "react-markdown"; +import React, {useState, useCallback, useMemo} from 'react' +import {Source, Layer} from 'react-map-gl' +import _ from 'lodash' +import {Button, Form, Dropdown, Header, Message, Icon} from 'semantic-ui-react' +import {useTranslation, Trans as Translate} from 'react-i18next' +import Markdown from 'react-markdown' -import { useConfig } from "config"; -import { Page, Map } from "components"; +import {useConfig} from 'config' +import {Page, Map} from 'components' -const BoundingBoxSelector = React.forwardRef( - ({ value, name, onChange }, ref) => { - const { t } = useTranslation(); - const [pointNum, setPointNum] = useState(0); - const [point0, setPoint0] = useState(null); - const [point1, setPoint1] = useState(null); +const BoundingBoxSelector = React.forwardRef(({value, name, onChange}, ref) => { + const {t} = useTranslation() + const [pointNum, setPointNum] = useState(0) + const [point0, setPoint0] = useState(null) + const [point1, setPoint1] = useState(null) - const onClick = (e) => { - if (pointNum == 0) { - setPoint0(e.lngLat); - } else { - setPoint1(e.lngLat); - } - setPointNum(1 - pointNum); - }; - - React.useEffect(() => { - if (!point0 || !point1) return; - const bbox = `${point0[0]},${point0[1]},${point1[0]},${point1[1]}`; - if (bbox !== value) { - onChange(bbox); - } - }, [point0, point1]); - - React.useEffect(() => { - if (!value) return; - const [p00, p01, p10, p11] = value - .split(",") - .map((v) => Number.parseFloat(v)); - if (!point0 || point0[0] != p00 || point0[1] != p01) - setPoint0([p00, p01]); - if (!point1 || point1[0] != p10 || point1[1] != p11) - setPoint1([p10, p11]); - }, [value]); - - return ( -
- onChange(e.target.value)} - /> - -
- - - - - -
-
- ); + const onClick = (e) => { + if (pointNum == 0) { + setPoint0(e.lngLat) + } else { + setPoint1(e.lngLat) + } + setPointNum(1 - pointNum) } -); -const MODES = ["events", "segments"]; -const FORMATS = ["geojson", "shapefile"]; + React.useEffect(() => { + if (!point0 || !point1) return + const bbox = `${point0[0]},${point0[1]},${point1[0]},${point1[1]}` + if (bbox !== value) { + onChange(bbox) + } + }, [point0, point1]) + + React.useEffect(() => { + if (!value) return + const [p00, p01, p10, p11] = value.split(',').map((v) => Number.parseFloat(v)) + if (!point0 || point0[0] != p00 || point0[1] != p01) setPoint0([p00, p01]) + if (!point1 || point1[0] != p10 || point1[1] != p11) setPoint1([p10, p11]) + }, [value]) + + return ( +
+ onChange(e.target.value)} + /> + +
+ + + + + +
+
+ ) +}) + +const MODES = ['events', 'segments'] +const FORMATS = ['geojson', 'shapefile'] export default function ExportPage() { - const [mode, setMode] = useState("events"); - const [bbox, setBbox] = useState("8.294678,49.651182,9.059601,50.108249"); - const [fmt, setFmt] = useState("geojson"); - const config = useConfig(); - const { t } = useTranslation(); + const [mode, setMode] = useState('events') + const [bbox, setBbox] = useState('8.294678,49.651182,9.059601,50.108249') + const [fmt, setFmt] = useState('geojson') + const config = useConfig() + const {t} = useTranslation() return ( -
{t("ExportPage.title")}
+
{t('ExportPage.title')}
- {t("ExportPage.information")} + {t('ExportPage.information')}
- + ({ @@ -137,14 +124,14 @@ export default function ExportPage() { value, }))} value={mode} - onChange={(_e, { value }) => setMode(value)} + onChange={(_e, {value}) => setMode(value)} /> - + ({ @@ -153,7 +140,7 @@ export default function ExportPage() { value, }))} value={fmt} - onChange={(_e, { value }) => setFmt(value)} + onChange={(_e, {value}) => setFmt(value)} /> @@ -170,5 +157,5 @@ export default function ExportPage() {
- ); + ) } diff --git a/frontend/src/pages/LoginRedirectPage.tsx b/frontend/src/pages/LoginRedirectPage.tsx index 4c3ead0..883d81a 100644 --- a/frontend/src/pages/LoginRedirectPage.tsx +++ b/frontend/src/pages/LoginRedirectPage.tsx @@ -1,69 +1,66 @@ -import React from "react"; -import { connect } from "react-redux"; -import { Redirect, useLocation, useHistory } from "react-router-dom"; -import { Icon, Message } from "semantic-ui-react"; -import { useObservable } from "rxjs-hooks"; -import { switchMap, pluck, distinctUntilChanged } from "rxjs/operators"; -import { useTranslation } from "react-i18next"; +import React from 'react' +import {connect} from 'react-redux' +import {Redirect, useLocation, useHistory} from 'react-router-dom' +import {Icon, Message} from 'semantic-ui-react' +import {useObservable} from 'rxjs-hooks' +import {switchMap, pluck, distinctUntilChanged} from 'rxjs/operators' +import {useTranslation} from 'react-i18next' -import { Page } from "components"; -import api from "api"; +import {Page} from 'components' +import api from 'api' const LoginRedirectPage = connect((state) => ({ loggedIn: Boolean(state.login), -}))(function LoginRedirectPage({ loggedIn }) { - const location = useLocation(); - const history = useHistory(); - const { search } = location; - const { t } = useTranslation(); +}))(function LoginRedirectPage({loggedIn}) { + const location = useLocation() + const history = useHistory() + const {search} = location + const {t} = useTranslation() /* eslint-disable react-hooks/exhaustive-deps */ // Hook dependency arrays in this block are intentionally left blank, we want // to keep the initial state, but reset the url once, ASAP, to not leak the // query parameters. This is considered good practice by OAuth. - const searchParams = React.useMemo( - () => Object.fromEntries(new URLSearchParams(search).entries()), - [] - ); + const searchParams = React.useMemo(() => Object.fromEntries(new URLSearchParams(search).entries()), []) React.useEffect(() => { - history.replace({ ...location, search: "" }); - }, []); + history.replace({...location, search: ''}) + }, []) /* eslint-enable react-hooks/exhaustive-deps */ if (loggedIn) { - return ; + return } - const { error, error_description: errorDescription, code } = searchParams; + const {error, error_description: errorDescription, code} = searchParams if (error) { return ( - + - ); + ) } - return ; -}); + return +}) -function LoginError({ errorText }: { errorText: string }) { - const { t } = useTranslation(); +function LoginError({errorText}: {errorText: string}) { + const {t} = useTranslation() return ( - {t("LoginRedirectPage.loginError")} - {t("LoginRedirectPage.loginErrorText", { error: errorText })} + {t('LoginRedirectPage.loginError')} + {t('LoginRedirectPage.loginErrorText', {error: errorText})} - ); + ) } -function ExchangeAuthCode({ code }) { - const { t } = useTranslation(); +function ExchangeAuthCode({code}) { + const {t} = useTranslation() const result = useObservable( (_$, args$) => args$.pipe( @@ -73,31 +70,31 @@ function ExchangeAuthCode({ code }) { ), null, [code] - ); + ) - let content; + let content if (result === null) { content = ( - {t("LoginRedirectPage.loggingIn")} - {t("LoginRedirectPage.hangTight")} + {t('LoginRedirectPage.loggingIn')} + {t('LoginRedirectPage.hangTight')} - ); + ) } else if (result === true) { - content = ; + content = } else { - const { error, error_description: errorDescription } = result; - content = ; + const {error, error_description: errorDescription} = result + content = } return ( {content} - ); + ) } -export default LoginRedirectPage; +export default LoginRedirectPage diff --git a/frontend/src/pages/MapPage/LayerSidebar.tsx b/frontend/src/pages/MapPage/LayerSidebar.tsx index 00286d2..8bccddc 100644 --- a/frontend/src/pages/MapPage/LayerSidebar.tsx +++ b/frontend/src/pages/MapPage/LayerSidebar.tsx @@ -1,81 +1,65 @@ -import React from "react"; -import _ from "lodash"; -import { connect } from "react-redux"; -import { Link } from "react-router-dom"; -import { - List, - Select, - Input, - Divider, - Label, - Checkbox, - Header, -} from "semantic-ui-react"; -import { useTranslation } from "react-i18next"; +import React from 'react' +import _ from 'lodash' +import {connect} from 'react-redux' +import {Link} from 'react-router-dom' +import {List, Select, Input, Divider, Label, Checkbox, Header} from 'semantic-ui-react' +import {useTranslation} from 'react-i18next' import { MapConfig, setMapConfigFlag as setMapConfigFlagAction, initialState as defaultMapConfig, -} from "reducers/mapConfig"; -import { colorByDistance, colorByCount, viridisSimpleHtml } from "mapstyles"; -import { ColorMapLegend, DiscreteColorMapLegend } from "components"; -import styles from "./styles.module.less"; +} from 'reducers/mapConfig' +import {colorByDistance, colorByCount, viridisSimpleHtml} from 'mapstyles' +import {ColorMapLegend, DiscreteColorMapLegend} from 'components' +import styles from './styles.module.less' -const BASEMAP_STYLE_OPTIONS = ["positron", "bright"]; +const BASEMAP_STYLE_OPTIONS = ['positron', 'bright'] const ROAD_ATTRIBUTE_OPTIONS = [ - "distance_overtaker_mean", - "distance_overtaker_min", - "distance_overtaker_max", - "distance_overtaker_median", - "overtaking_event_count", - "usage_count", - "zone", -]; + 'distance_overtaker_mean', + 'distance_overtaker_min', + 'distance_overtaker_max', + 'distance_overtaker_median', + 'overtaking_event_count', + 'usage_count', + 'zone', +] -const DATE_FILTER_MODES = ["none", "range", "threshold"]; +const DATE_FILTER_MODES = ['none', 'range', 'threshold'] -type User = Object; +type User = Object function LayerSidebar({ mapConfig, login, setMapConfigFlag, }: { - login: User | null; - mapConfig: MapConfig; - setMapConfigFlag: (flag: string, value: unknown) => void; + login: User | null + mapConfig: MapConfig + setMapConfigFlag: (flag: string, value: unknown) => void }) { - const { t } = useTranslation(); + const {t} = useTranslation() const { - baseMap: { style }, - obsRoads: { show: showRoads, showUntagged, attribute, maxCount }, - obsEvents: { show: showEvents }, - obsRegions: { show: showRegions }, - filters: { - currentUser: filtersCurrentUser, - dateMode, - startDate, - endDate, - thresholdAfter, - }, - } = mapConfig; + baseMap: {style}, + obsRoads: {show: showRoads, showUntagged, attribute, maxCount}, + obsEvents: {show: showEvents}, + obsRegions: {show: showRegions}, + filters: {currentUser: filtersCurrentUser, dateMode, startDate, endDate, thresholdAfter}, + } = mapConfig const openStreetMapCopyright = ( - {t("MapPage.sidebar.copyright.openStreetMap")}{" "} - - {t("MapPage.sidebar.copyright.learnMore")} - + {t('MapPage.sidebar.copyright.openStreetMap')}{' '} + {t('MapPage.sidebar.copyright.learnMore')} - ); + ) return (
- {t("MapPage.sidebar.baseMap.style.label")} + {t('MapPage.sidebar.baseMap.style.label')} ({ @@ -163,74 +137,50 @@ function LayerSidebar({ text: t(`MapPage.sidebar.obsRoads.attribute.${value}`), }))} value={attribute} - onChange={(_e, { value }) => - setMapConfigFlag("obsRoads.attribute", value) - } + onChange={(_e, {value}) => setMapConfigFlag('obsRoads.attribute', value)} /> - {attribute.endsWith("_count") ? ( + {attribute.endsWith('_count') ? ( <> - - {t("MapPage.sidebar.obsRoads.maxCount.label")} - + {t('MapPage.sidebar.obsRoads.maxCount.label')} - setMapConfigFlag("obsRoads.maxCount", value) - } + onChange={(_e, {value}) => setMapConfigFlag('obsRoads.maxCount', value)} /> - ) : attribute.endsWith("zone") ? ( + ) : attribute.endsWith('zone') ? ( <> - ) : ( <> - - {_.upperFirst(t("general.zone.urban"))} - - + {_.upperFirst(t('general.zone.urban'))} + - - {_.upperFirst(t("general.zone.rural"))} - - + {_.upperFirst(t('general.zone.rural'))} + )} @@ -243,40 +193,36 @@ function LayerSidebar({ toggle size="small" id="obsEvents.show" - style={{ float: "right" }} + style={{float: 'right'}} checked={showEvents} - onChange={() => setMapConfigFlag("obsEvents.show", !showEvents)} + onChange={() => setMapConfigFlag('obsEvents.show', !showEvents)} /> {showEvents && ( <> - {_.upperFirst(t("general.zone.urban"))} - + {_.upperFirst(t('general.zone.urban'))} + - {_.upperFirst(t("general.zone.rural"))} - + {_.upperFirst(t('general.zone.rural'))} + )} -
{t("MapPage.sidebar.filters.title")}
+
{t('MapPage.sidebar.filters.title')}
{login && ( <> -
{t("MapPage.sidebar.filters.userData")}
+
{t('MapPage.sidebar.filters.userData')}
@@ -285,15 +231,13 @@ function LayerSidebar({ size="small" id="filters.currentUser" checked={filtersCurrentUser} - onChange={() => - setMapConfigFlag("filters.currentUser", !filtersCurrentUser) - } - label={t("MapPage.sidebar.filters.currentUser")} + onChange={() => setMapConfigFlag('filters.currentUser', !filtersCurrentUser)} + label={t('MapPage.sidebar.filters.currentUser')} /> -
{t("MapPage.sidebar.filters.dateRange")}
+
{t('MapPage.sidebar.filters.dateRange')}
@@ -304,14 +248,12 @@ function LayerSidebar({ key: value, text: t(`MapPage.sidebar.filters.dateMode.${value}`), }))} - value={dateMode ?? "none"} - onChange={(_e, { value }) => - setMapConfigFlag("filters.dateMode", value) - } + value={dateMode ?? 'none'} + onChange={(_e, {value}) => setMapConfigFlag('filters.dateMode', value)} /> - {dateMode == "range" && ( + {dateMode == 'range' && ( - setMapConfigFlag("filters.startDate", value) - } + onChange={(_e, {value}) => setMapConfigFlag('filters.startDate', value)} value={startDate ?? null} - label={t("MapPage.sidebar.filters.start")} + label={t('MapPage.sidebar.filters.start')} /> )} - {dateMode == "range" && ( + {dateMode == 'range' && ( - setMapConfigFlag("filters.endDate", value) - } + onChange={(_e, {value}) => setMapConfigFlag('filters.endDate', value)} value={endDate ?? null} - label={t("MapPage.sidebar.filters.end")} + label={t('MapPage.sidebar.filters.end')} /> )} - {dateMode == "threshold" && ( + {dateMode == 'threshold' && ( - setMapConfigFlag("filters.startDate", value) - } - label={t("MapPage.sidebar.filters.threshold")} + onChange={(_e, {value}) => setMapConfigFlag('filters.startDate', value)} + label={t('MapPage.sidebar.filters.threshold')} /> )} - {dateMode == "threshold" && ( + {dateMode == 'threshold' && ( - {t("MapPage.sidebar.filters.before")}{" "} + {t('MapPage.sidebar.filters.before')}{' '} - setMapConfigFlag( - "filters.thresholdAfter", - !thresholdAfter - ) - } + onChange={() => setMapConfigFlag('filters.thresholdAfter', !thresholdAfter)} id="filters.thresholdAfter" - />{" "} - {t("MapPage.sidebar.filters.after")} + />{' '} + {t('MapPage.sidebar.filters.after')} )} )} - {!login && ( - {t("MapPage.sidebar.filters.needsLogin")} - )} + {!login && {t('MapPage.sidebar.filters.needsLogin')}}
- ); + ) } export default connect( @@ -402,6 +331,6 @@ export default connect( ), login: state.login, }), - { setMapConfigFlag: setMapConfigFlagAction } + {setMapConfigFlag: setMapConfigFlagAction} // -)(LayerSidebar); +)(LayerSidebar) diff --git a/frontend/src/pages/MapPage/index.tsx b/frontend/src/pages/MapPage/index.tsx index a10427a..b3517ab 100644 --- a/frontend/src/pages/MapPage/index.tsx +++ b/frontend/src/pages/MapPage/index.tsx @@ -1,296 +1,253 @@ -import React, { useState, useCallback, useMemo, useRef } from "react"; -import _ from "lodash"; -import { connect } from "react-redux"; -import { Button } from "semantic-ui-react"; -import { Layer, Source } from "react-map-gl"; -import produce from "immer"; -import classNames from "classnames"; +import React, {useState, useCallback, useMemo, useRef} from 'react' +import _ from 'lodash' +import {connect} from 'react-redux' +import {Button} from 'semantic-ui-react' +import {Layer, Source} from 'react-map-gl' +import produce from 'immer' +import classNames from 'classnames' -import api from "api"; -import type { Location } from "types"; -import { Page, Map } from "components"; -import { useConfig } from "config"; -import { - colorByDistance, - colorByCount, - getRegionLayers, - borderByZone, - isValidAttribute, -} from "mapstyles"; -import { useMapConfig } from "reducers/mapConfig"; +import api from 'api' +import type {Location} from 'types' +import {Page, Map} from 'components' +import {useConfig} from 'config' +import {colorByDistance, colorByCount, getRegionLayers, borderByZone, isValidAttribute} from 'mapstyles' +import {useMapConfig} from 'reducers/mapConfig' -import RoadInfo, { RoadInfoType } from "./RoadInfo"; -import RegionInfo from "./RegionInfo"; -import LayerSidebar from "./LayerSidebar"; -import styles from "./styles.module.less"; +import RoadInfo, {RoadInfoType} from './RoadInfo' +import RegionInfo from './RegionInfo' +import LayerSidebar from './LayerSidebar' +import styles from './styles.module.less' const untaggedRoadsLayer = { - id: "obs_roads_untagged", - type: "line", - source: "obs", - "source-layer": "obs_roads", + id: 'obs_roads_untagged', + type: 'line', + source: 'obs', + 'source-layer': 'obs_roads', minzoom: 12, - filter: ["!", ["to-boolean", ["get", "distance_overtaker_mean"]]], + filter: ['!', ['to-boolean', ['get', 'distance_overtaker_mean']]], layout: { - "line-cap": "round", - "line-join": "round", + 'line-cap': 'round', + 'line-join': 'round', }, paint: { - "line-width": ["interpolate", ["exponential", 1.5], ["zoom"], 12, 2, 17, 2], - "line-color": "#ABC", + 'line-width': ['interpolate', ['exponential', 1.5], ['zoom'], 12, 2, 17, 2], + 'line-color': '#ABC', // "line-opacity": ["interpolate", ["linear"], ["zoom"], 14, 0, 15, 1], - "line-offset": [ - "interpolate", - ["exponential", 1.5], - ["zoom"], + 'line-offset': [ + 'interpolate', + ['exponential', 1.5], + ['zoom'], 12, - ["get", "offset_direction"], + ['get', 'offset_direction'], 19, - ["*", ["get", "offset_direction"], 8], + ['*', ['get', 'offset_direction'], 8], ], }, -}; +} const getUntaggedRoadsLayer = (colorAttribute) => produce(untaggedRoadsLayer, (draft) => { - draft.filter = ["!", isValidAttribute(colorAttribute)]; - }); + draft.filter = ['!', isValidAttribute(colorAttribute)] + }) const getRoadsLayer = (colorAttribute, maxCount) => produce(untaggedRoadsLayer, (draft) => { - draft.id = "obs_roads_normal"; - draft.filter = isValidAttribute(colorAttribute); - draft.minzoom = 10; - draft.paint["line-width"][6] = 6; // scale bigger on zoom - draft.paint["line-color"] = colorAttribute.startsWith("distance_") + draft.id = 'obs_roads_normal' + draft.filter = isValidAttribute(colorAttribute) + draft.minzoom = 10 + draft.paint['line-width'][6] = 6 // scale bigger on zoom + draft.paint['line-color'] = colorAttribute.startsWith('distance_') ? colorByDistance(colorAttribute) - : colorAttribute.endsWith("_count") + : colorAttribute.endsWith('_count') ? colorByCount(colorAttribute, maxCount) - : colorAttribute.endsWith("zone") + : colorAttribute.endsWith('zone') ? borderByZone() - : "#DDD"; + : '#DDD' // draft.paint["line-opacity"][3] = 12; // draft.paint["line-opacity"][5] = 13; - }); + }) const getEventsLayer = () => ({ - id: "obs_events", - type: "circle", - source: "obs", - "source-layer": "obs_events", + id: 'obs_events', + type: 'circle', + source: 'obs', + 'source-layer': 'obs_events', paint: { - "circle-radius": ["interpolate", ["linear"], ["zoom"], 14, 3, 17, 8], - "circle-opacity": ["interpolate",["linear"],["zoom"],8,0.1,9,0.3,10,0.5,11,1], - "circle-color": colorByDistance("distance_overtaker"), + 'circle-radius': ['interpolate', ['linear'], ['zoom'], 14, 3, 17, 8], + 'circle-opacity': ['interpolate', ['linear'], ['zoom'], 8, 0.1, 9, 0.3, 10, 0.5, 11, 1], + 'circle-color': colorByDistance('distance_overtaker'), }, minzoom: 8, -}); +}) const getEventsTextLayer = () => ({ - id: "obs_events_text", - type: "symbol", + id: 'obs_events_text', + type: 'symbol', minzoom: 18, - source: "obs", - "source-layer": "obs_events", + source: 'obs', + 'source-layer': 'obs_events', layout: { - "text-field": [ - "number-format", - ["get", "distance_overtaker"], - { "min-fraction-digits": 2, "max-fraction-digits": 2 }, + 'text-field': [ + 'number-format', + ['get', 'distance_overtaker'], + {'min-fraction-digits': 2, 'max-fraction-digits': 2}, ], - "text-allow-overlap": true, - "text-size": 14, - "text-keep-upright": false, - "text-anchor": "left", - "text-radial-offset": 1, - "text-rotate": ["-", 90, ["*", ["get", "course"], 180 / Math.PI]], - "text-rotation-alignment": "map", + 'text-allow-overlap': true, + 'text-size': 14, + 'text-keep-upright': false, + 'text-anchor': 'left', + 'text-radial-offset': 1, + 'text-rotate': ['-', 90, ['*', ['get', 'course'], 180 / Math.PI]], + 'text-rotation-alignment': 'map', }, paint: { - "text-halo-color": "rgba(255, 255, 255, 1)", - "text-halo-width": 1, - "text-opacity": ["interpolate", ["linear"], ["zoom"], 15, 0, 15.3, 1], + 'text-halo-color': 'rgba(255, 255, 255, 1)', + 'text-halo-width': 1, + 'text-opacity': ['interpolate', ['linear'], ['zoom'], 15, 0, 15.3, 1], }, -}); +}) interface RegionInfo { properties: { - admin_level: number; - name: string; - overtaking_event_count: number; - }; + admin_level: number + name: string + overtaking_event_count: number + } } -type Details = - | { type: "road"; road: RoadInfoType } - | { type: "region"; region: RegionInfo }; +type Details = {type: 'road'; road: RoadInfoType} | {type: 'region'; region: RegionInfo} -function MapPage({ login }) { - const { obsMapSource, banner } = useConfig() || {}; - const [details, setDetails] = useState(null); +function MapPage({login}) { + const {obsMapSource, banner} = useConfig() || {} + const [details, setDetails] = useState(null) - const onCloseDetails = useCallback(() => setDetails(null), [setDetails]); + const onCloseDetails = useCallback(() => setDetails(null), [setDetails]) - const mapConfig = useMapConfig(); + const mapConfig = useMapConfig() - const viewportRef = useRef(); - const mapInfoPortal = useRef(); + const viewportRef = useRef() + const mapInfoPortal = useRef() const onViewportChange = useCallback( (viewport) => { - viewportRef.current = viewport; + viewportRef.current = viewport }, [viewportRef] - ); + ) const onClick = useCallback( async (e) => { // check if we clicked inside the mapInfoBox, if so, early exit - let node = e.target; + let node = e.target while (node) { - if ( - [styles.mapInfoBox, styles.mapToolbar].some((className) => - node?.classList?.contains(className) - ) - ) { - return; + if ([styles.mapInfoBox, styles.mapToolbar].some((className) => node?.classList?.contains(className))) { + return } - node = node.parentNode; + node = node.parentNode } - const { zoom } = viewportRef.current; + const {zoom} = viewportRef.current if (zoom < 10) { - const clickedRegion = e.features?.find( - (f) => f.source === "obs" && f.sourceLayer === "obs_regions" - ); - setDetails( - clickedRegion ? { type: "region", region: clickedRegion } : null - ); + const clickedRegion = e.features?.find((f) => f.source === 'obs' && f.sourceLayer === 'obs_regions') + setDetails(clickedRegion ? {type: 'region', region: clickedRegion} : null) } else { - const road = await api.get("/mapdetails/road", { + const road = await api.get('/mapdetails/road', { query: { longitude: e.lngLat[0], latitude: e.lngLat[1], radius: 100, }, - }); - setDetails(road?.road ? { type: "road", road } : null); + }) + setDetails(road?.road ? {type: 'road', road} : null) } }, [setDetails] - ); + ) - const [layerSidebar, setLayerSidebar] = useState(true); + const [layerSidebar, setLayerSidebar] = useState(true) const { - obsRoads: { attribute, maxCount }, - } = mapConfig; + obsRoads: {attribute, maxCount}, + } = mapConfig - const layers = []; + const layers = [] - const untaggedRoadsLayerCustom = useMemo( - () => getUntaggedRoadsLayer(attribute), - [attribute] - ); + const untaggedRoadsLayerCustom = useMemo(() => getUntaggedRoadsLayer(attribute), [attribute]) if (mapConfig.obsRoads.show && mapConfig.obsRoads.showUntagged) { - layers.push(untaggedRoadsLayerCustom); + layers.push(untaggedRoadsLayerCustom) } - const roadsLayer = useMemo( - () => getRoadsLayer(attribute, maxCount), - [attribute, maxCount] - ); + const roadsLayer = useMemo(() => getRoadsLayer(attribute, maxCount), [attribute, maxCount]) if (mapConfig.obsRoads.show) { - layers.push(roadsLayer); + layers.push(roadsLayer) } - const regionLayers = useMemo(() => getRegionLayers(), []); + const regionLayers = useMemo(() => getRegionLayers(), []) if (mapConfig.obsRegions.show) { - layers.push(...regionLayers); + layers.push(...regionLayers) } - const eventsLayer = useMemo(() => getEventsLayer(), []); - const eventsTextLayer = useMemo(() => getEventsTextLayer(), []); + const eventsLayer = useMemo(() => getEventsLayer(), []) + const eventsTextLayer = useMemo(() => getEventsTextLayer(), []) if (mapConfig.obsEvents.show) { - layers.push(eventsLayer); - layers.push(eventsTextLayer); + layers.push(eventsLayer) + layers.push(eventsTextLayer) } const onToggleLayerSidebarButtonClick = useCallback( (e) => { - e.stopPropagation(); - e.preventDefault(); - console.log("toggl;e"); - setLayerSidebar((v) => !v); + e.stopPropagation() + e.preventDefault() + console.log('toggl;e') + setLayerSidebar((v) => !v) }, [setLayerSidebar] - ); + ) if (!obsMapSource) { - return null; + return null } const tiles = obsMapSource?.tiles?.map((tileUrl: string) => { - const query = new URLSearchParams(); + const query = new URLSearchParams() if (login) { if (mapConfig.filters.currentUser) { - query.append("user", login.id); + query.append('user', login.id) } - if (mapConfig.filters.dateMode === "range") { + if (mapConfig.filters.dateMode === 'range') { if (mapConfig.filters.startDate) { - query.append("start", mapConfig.filters.startDate); + query.append('start', mapConfig.filters.startDate) } if (mapConfig.filters.endDate) { - query.append("end", mapConfig.filters.endDate); + query.append('end', mapConfig.filters.endDate) } - } else if (mapConfig.filters.dateMode === "threshold") { + } else if (mapConfig.filters.dateMode === 'threshold') { if (mapConfig.filters.startDate) { - query.append( - mapConfig.filters.thresholdAfter ? "start" : "end", - mapConfig.filters.startDate - ); + query.append(mapConfig.filters.thresholdAfter ? 'start' : 'end', mapConfig.filters.startDate) } } } - const queryString = String(query); - return tileUrl + (queryString ? "?" : "") + queryString; - }); + const queryString = String(query) + return tileUrl + (queryString ? '?' : '') + queryString + }) - const hasFilters: boolean = - login && - (mapConfig.filters.currentUser || mapConfig.filters.dateMode !== "none"); + const hasFilters: boolean = login && (mapConfig.filters.currentUser || mapConfig.filters.dateMode !== 'none') return ( -
+
{layerSidebar && (
)}
- +
-
{layers.map((layer) => ( @@ -298,27 +255,23 @@ function MapPage({ login }) { ))} - {details?.type === "road" && details?.road?.road && ( + {details?.type === 'road' && details?.road?.road && ( )} - {details?.type === "region" && details?.region && ( - + {details?.type === 'region' && details?.region && ( + )}
- ); + ) } -export default connect((state) => ({ login: state.login }))(MapPage); +export default connect((state) => ({login: state.login}))(MapPage) diff --git a/frontend/src/pages/MyTracksPage.tsx b/frontend/src/pages/MyTracksPage.tsx index 31bdcc7..bc80115 100644 --- a/frontend/src/pages/MyTracksPage.tsx +++ b/frontend/src/pages/MyTracksPage.tsx @@ -1,5 +1,5 @@ -import React, { useCallback, useMemo, useState } from "react"; -import { connect } from "react-redux"; +import React, {useCallback, useMemo, useState} from 'react' +import {connect} from 'react-redux' import { Accordion, Button, @@ -14,74 +14,65 @@ import { SemanticCOLORS, SemanticICONS, Table, -} from "semantic-ui-react"; -import { useObservable } from "rxjs-hooks"; -import { Link } from "react-router-dom"; -import { of, from, concat, BehaviorSubject, combineLatest } from "rxjs"; -import { map, switchMap, distinctUntilChanged } from "rxjs/operators"; -import _ from "lodash"; -import { useTranslation } from "react-i18next"; +} from 'semantic-ui-react' +import {useObservable} from 'rxjs-hooks' +import {Link} from 'react-router-dom' +import {of, from, concat, BehaviorSubject, combineLatest} from 'rxjs' +import {map, switchMap, distinctUntilChanged} from 'rxjs/operators' +import _ from 'lodash' +import {useTranslation} from 'react-i18next' -import type { ProcessingStatus, Track, UserDevice } from "types"; -import { Page, FormattedDate, Visibility } from "components"; -import api from "api"; -import { useCallbackRef, formatDistance, formatDuration } from "utils"; +import type {ProcessingStatus, Track, UserDevice} from 'types' +import {Page, FormattedDate, Visibility} from 'components' +import api from 'api' +import {useCallbackRef, formatDistance, formatDuration} from 'utils' -import download from "downloadjs"; +import download from 'downloadjs' const COLOR_BY_STATUS: Record = { - error: "red", - complete: "green", - created: "grey", - queued: "orange", - processing: "orange", -}; + error: 'red', + complete: 'green', + created: 'grey', + queued: 'orange', + processing: 'orange', +} const ICON_BY_STATUS: Record = { - error: "warning sign", - complete: "check circle outline", - created: "bolt", - queued: "bolt", - processing: "bolt", -}; + error: 'warning sign', + complete: 'check circle outline', + created: 'bolt', + queued: 'bolt', + processing: 'bolt', +} -function ProcessingStatusLabel({ status }: { status: ProcessingStatus }) { - const { t } = useTranslation(); +function ProcessingStatusLabel({status}: {status: ProcessingStatus}) { + const {t} = useTranslation() return ( - ); + ) } -function SortableHeader({ - children, - setOrderBy, - orderBy, - reversed, - setReversed, - name, - ...props -}) { +function SortableHeader({children, setOrderBy, orderBy, reversed, setReversed, name, ...props}) { const toggleSort = (e) => { - e.preventDefault(); - e.stopPropagation(); + e.preventDefault() + e.stopPropagation() if (orderBy === name) { if (!reversed) { - setReversed(true); + setReversed(true) } else { - setReversed(false); - setOrderBy(null); + setReversed(false) + setOrderBy(null) } } else { - setReversed(false); - setOrderBy(name); + setReversed(false) + setOrderBy(name) } - }; + } - let icon = - orderBy === name ? (reversed ? "sort descending" : "sort ascending") : null; + let icon = orderBy === name ? (reversed ? 'sort descending' : 'sort ascending') : null return ( @@ -90,22 +81,22 @@ function SortableHeader({
- ); + ) } type Filters = { - userDeviceId?: null | number; - visibility?: null | boolean; -}; + userDeviceId?: null | number + visibility?: null | boolean +} function TrackFilters({ filters, setFilters, deviceNames, }: { - filters: Filters; - setFilters: (f: Filters) => void; - deviceNames: null | Record; + filters: Filters + setFilters: (f: Filters) => void + deviceNames: null | Record }) { return ( @@ -115,19 +106,15 @@ function TrackFilters({ selection clearable options={[ - { value: 0, key: "__none__", text: "All my devices" }, - ..._.sortBy(Object.entries(deviceNames ?? {}), 1).map( - ([deviceId, deviceName]: [string, string]) => ({ - value: Number(deviceId), - key: deviceId, - text: deviceName, - }) - ), + {value: 0, key: '__none__', text: 'All my devices'}, + ..._.sortBy(Object.entries(deviceNames ?? {}), 1).map(([deviceId, deviceName]: [string, string]) => ({ + value: Number(deviceId), + key: deviceId, + text: deviceName, + })), ]} value={filters?.userDeviceId ?? 0} - onChange={(_e, { value }) => - setFilters({ ...filters, userDeviceId: (value as number) || null }) - } + onChange={(_e, {value}) => setFilters({...filters, userDeviceId: (value as number) || null})} /> @@ -137,54 +124,48 @@ function TrackFilters({ selection clearable options={[ - { value: "none", key: "any", text: "Any" }, - { value: true, key: "public", text: "Public" }, - { value: false, key: "private", text: "Private" }, + {value: 'none', key: 'any', text: 'Any'}, + {value: true, key: 'public', text: 'Public'}, + {value: false, key: 'private', text: 'Private'}, ]} - value={filters?.visibility ?? "none"} - onChange={(_e, { value }) => + value={filters?.visibility ?? 'none'} + onChange={(_e, {value}) => setFilters({ ...filters, - visibility: value === "none" ? null : (value as boolean), + visibility: value === 'none' ? null : (value as boolean), }) } /> - ); + ) } -function TracksTable({ title }) { - const [orderBy, setOrderBy] = useState("recordedAt"); - const [reversed, setReversed] = useState(true); - const [showFilters, setShowFilters] = useState(false); - const [filters, setFilters] = useState({}); - const [selectedTracks, setSelectedTracks] = useState>( - {} - ); +function TracksTable({title}) { + const [orderBy, setOrderBy] = useState('recordedAt') + const [reversed, setReversed] = useState(true) + const [showFilters, setShowFilters] = useState(false) + const [filters, setFilters] = useState({}) + const [selectedTracks, setSelectedTracks] = useState>({}) - const toggleTrackSelection = useCallbackRef( - (slug: string, selected?: boolean) => { - const newSelected = selected ?? !selectedTracks[slug]; - setSelectedTracks( - _.pickBy({ ...selectedTracks, [slug]: newSelected }, _.identity) - ); - } - ); + const toggleTrackSelection = useCallbackRef((slug: string, selected?: boolean) => { + const newSelected = selected ?? !selectedTracks[slug] + setSelectedTracks(_.pickBy({...selectedTracks, [slug]: newSelected}, _.identity)) + }) const query = _.pickBy( { limit: 1000, offset: 0, order_by: orderBy, - reversed: reversed ? "true" : "false", + reversed: reversed ? 'true' : 'false', user_device_id: filters?.userDeviceId, public: filters?.visibility, }, (x) => x != null - ); + ) - const forceUpdate$ = useMemo(() => new BehaviorSubject(null), []); + const forceUpdate$ = useMemo(() => new BehaviorSubject(null), []) const tracks: Track[] | null = useObservable( (_$, inputs$) => combineLatest([ @@ -193,125 +174,91 @@ function TracksTable({ title }) { distinctUntilChanged(_.isEqual) ), forceUpdate$, - ]).pipe( - switchMap(([query]) => - concat( - of(null), - from(api.get("/tracks/feed", { query }).then((r) => r.tracks)) - ) - ) - ), + ]).pipe(switchMap(([query]) => concat(of(null), from(api.get('/tracks/feed', {query}).then((r) => r.tracks))))), null, [query] - ); + ) const deviceNames: null | Record = useObservable(() => - from(api.get("/user/devices")).pipe( + from(api.get('/user/devices')).pipe( map((response: UserDevice[]) => - Object.fromEntries( - response.map((device) => [ - device.id, - device.displayName || device.identifier, - ]) - ) + Object.fromEntries(response.map((device) => [device.id, device.displayName || device.identifier])) ) ) - ); + ) - const { t } = useTranslation(); + const {t} = useTranslation() - const p = { orderBy, setOrderBy, reversed, setReversed }; + const p = {orderBy, setOrderBy, reversed, setReversed} - const selectedCount = Object.keys(selectedTracks).length; - const noneSelected = selectedCount === 0; - const allSelected = selectedCount === tracks?.length; + const selectedCount = Object.keys(selectedTracks).length + const noneSelected = selectedCount === 0 + const allSelected = selectedCount === tracks?.length const selectAll = () => { - setSelectedTracks( - Object.fromEntries(tracks?.map((t) => [t.slug, true]) ?? []) - ); - }; + setSelectedTracks(Object.fromEntries(tracks?.map((t) => [t.slug, true]) ?? [])) + } const selectNone = () => { - setSelectedTracks({}); - }; + setSelectedTracks({}) + } const bulkAction = async (action: string) => { - const response = await api.post("/tracks/bulk", { + const response = await api.post('/tracks/bulk', { body: { action, tracks: Object.keys(selectedTracks), }, returnResponse: true, - }); - if (action === "download") { - const contentType = - response.headers.get("content-type") ?? "application/x-gtar"; + }) + if (action === 'download') { + const contentType = response.headers.get('content-type') ?? 'application/x-gtar' - const filename = - response.headers - .get("content-disposition") - ?.match(/filename="([^"]+)"/)?.[1] ?? "tracks.tar.bz2"; - download(await response.blob(), filename, contentType); + const filename = response.headers.get('content-disposition')?.match(/filename="([^"]+)"/)?.[1] ?? 'tracks.tar.bz2' + download(await response.blob(), filename, contentType) } - setShowBulkDelete(false); - setSelectedTracks({}); - forceUpdate$.next(null); - }; - const [showBulkDelete, setShowBulkDelete] = useState(false); + setShowBulkDelete(false) + setSelectedTracks({}) + forceUpdate$.next(null) + } + const [showBulkDelete, setShowBulkDelete] = useState(false) return ( <> -
+
- - Selection of {selectedCount} tracks - - bulkAction("makePrivate")}> - Make private - - bulkAction("makePublic")}> - Make public - - bulkAction("reprocess")}> - Reprocess - - bulkAction("download")}> - Download - - setShowBulkDelete(true)}> - Delete - + Selection of {selectedCount} tracks + bulkAction('makePrivate')}>Make private + bulkAction('makePublic')}>Make public + bulkAction('reprocess')}>Reprocess + bulkAction('download')}>Download + setShowBulkDelete(true)}>Delete
{title}
-
- +
+ - setShowFilters(!showFilters)} - > + setShowFilters(!showFilters)}> Filters - + setShowBulkDelete(false)} - onConfirm={() => bulkAction("delete")} + onConfirm={() => bulkAction('delete')} content={`Are you sure you want to delete ${selectedCount} tracks?`} - confirmButton={t("general.delete")} - cancelButton={t("general.cancel")} + confirmButton={t('general.delete')} + cancelButton={t('general.cancel')} /> @@ -356,11 +303,9 @@ function TracksTable({ title }) { /> - {track.processingStatus == null ? null : ( - - )} + {track.processingStatus == null ? null : } - {track.title || t("general.unnamedTrack")} + {track.title || t('general.unnamedTrack')} @@ -368,62 +313,48 @@ function TracksTable({ title }) { - - {track.public == null ? null : ( - - )} - + {track.public == null ? null : } - - {formatDistance(track.length)} - + {formatDistance(track.length)} - - {formatDuration(track.duration)} - + {formatDuration(track.duration)} - - {track.userDeviceId - ? deviceNames?.[track.userDeviceId] ?? "..." - : null} - + {track.userDeviceId ? deviceNames?.[track.userDeviceId] ?? '...' : null} ))}
- ); + ) } -function UploadButton({ navigate, ...props }) { - const { t } = useTranslation(); +function UploadButton({navigate, ...props}) { + const {t} = useTranslation() const onClick = useCallback( (e) => { - e.preventDefault(); - navigate(); + e.preventDefault() + navigate() }, [navigate] - ); + ) return ( - ); + ) } -const MyTracksPage = connect((state) => ({ login: (state as any).login }))( - function MyTracksPage({ login }) { - const { t } = useTranslation(); +const MyTracksPage = connect((state) => ({login: (state as any).login}))(function MyTracksPage({login}) { + const {t} = useTranslation() - const title = t("TracksPage.titleUser"); + const title = t('TracksPage.titleUser') - return ( - - - - ); - } -); + return ( + + + + ) +}) -export default MyTracksPage; +export default MyTracksPage diff --git a/frontend/src/pages/NotFoundPage.js b/frontend/src/pages/NotFoundPage.js index 6e412c2..dcf459b 100644 --- a/frontend/src/pages/NotFoundPage.js +++ b/frontend/src/pages/NotFoundPage.js @@ -1,12 +1,12 @@ import React from 'react' import {Button, Header} from 'semantic-ui-react' import {useHistory} from 'react-router-dom' -import { useTranslation } from "react-i18next"; +import {useTranslation} from 'react-i18next' import {Page} from '../components' export default function NotFoundPage() { - const { t } = useTranslation(); + const {t} = useTranslation() const history = useHistory() return ( diff --git a/frontend/src/pages/SettingsPage/ApiKeySettings.tsx b/frontend/src/pages/SettingsPage/ApiKeySettings.tsx index b3f60ce..b21e722 100644 --- a/frontend/src/pages/SettingsPage/ApiKeySettings.tsx +++ b/frontend/src/pages/SettingsPage/ApiKeySettings.tsx @@ -1,125 +1,102 @@ -import React from "react"; -import { connect } from "react-redux"; -import { - Message, - Icon, - Button, - Ref, - Input, - Segment, - Popup, -} from "semantic-ui-react"; -import Markdown from "react-markdown"; -import { useTranslation } from "react-i18next"; +import React from 'react' +import {connect} from 'react-redux' +import {Message, Icon, Button, Ref, Input, Segment, Popup} from 'semantic-ui-react' +import Markdown from 'react-markdown' +import {useTranslation} from 'react-i18next' -import { setLogin } from "reducers/login"; -import api from "api"; -import { findInput } from "utils"; -import { useConfig } from "config"; +import {setLogin} from 'reducers/login' +import api from 'api' +import {findInput} from 'utils' +import {useConfig} from 'config' -function CopyInput({ value, ...props }) { - const { t } = useTranslation(); - const [success, setSuccess] = React.useState(null); +function CopyInput({value, ...props}) { + const {t} = useTranslation() + const [success, setSuccess] = React.useState(null) const onClick = async () => { try { - await window.navigator?.clipboard?.writeText(value); - setSuccess(true); + await window.navigator?.clipboard?.writeText(value) + setSuccess(true) } catch (err) { - setSuccess(false); + setSuccess(false) } finally { setTimeout(() => { - setSuccess(null); - }, 2000); + setSuccess(null) + }, 2000) } - }; + } return ( - } + trigger={} position="top right" open={success != null} - content={success ? t("general.copied") : t("general.copyError")} + content={success ? t('general.copied') : t('general.copyError')} /> - ); + ) } -const selectField = findInput((ref) => ref?.select()); +const selectField = findInput((ref) => ref?.select()) -const ApiKeySettings = connect((state) => ({ login: state.login }), { +const ApiKeySettings = connect((state) => ({login: state.login}), { setLogin, -})(function ApiKeySettings({ login, setLogin, setErrors }) { - const { t } = useTranslation(); - const [loading, setLoading] = React.useState(false); - const config = useConfig(); - const [show, setShow] = React.useState(false); +})(function ApiKeySettings({login, setLogin, setErrors}) { + const {t} = useTranslation() + const [loading, setLoading] = React.useState(false) + const config = useConfig() + const [show, setShow] = React.useState(false) const onClick = React.useCallback( (e) => { - e.preventDefault(); - setShow(true); + e.preventDefault() + setShow(true) }, [setShow] - ); + ) const onGenerateNewKey = React.useCallback( async (e) => { - e.preventDefault(); - setLoading(true); + e.preventDefault() + setLoading(true) try { - const response = await api.put("/user", { - body: { updateApiKey: true }, - }); - setLogin(response); + const response = await api.put('/user', { + body: {updateApiKey: true}, + }) + setLogin(response) } catch (err) { - setErrors(err.errors); + setErrors(err.errors) } finally { - setLoading(false); + setLoading(false) } }, [setLoading, setLogin, setErrors] - ); + ) return ( - - {t("SettingsPage.apiKey.description")} -
+ + {t('SettingsPage.apiKey.description')} +
{show ? ( login.apiKey ? ( - + ) : ( - + ) ) : ( )}
- {t("SettingsPage.apiKey.urlDescription")} -
- + {t('SettingsPage.apiKey.urlDescription')} +
+
- {t("SettingsPage.apiKey.generateDescription")} + {t('SettingsPage.apiKey.generateDescription')}

- + - ); -}); + ) +}) -export default ApiKeySettings; +export default ApiKeySettings diff --git a/frontend/src/pages/SettingsPage/DeviceList.tsx b/frontend/src/pages/SettingsPage/DeviceList.tsx index 6200c03..d2f967d 100644 --- a/frontend/src/pages/SettingsPage/DeviceList.tsx +++ b/frontend/src/pages/SettingsPage/DeviceList.tsx @@ -27,7 +27,7 @@ function EditField({value, onEdit}) { }, [setEditing, setTempValue, value, cancelTimeout]) const confirm = useCallback(() => { - console.log("confirmed") + console.log('confirmed') cancelTimeout() setEditing(false) onEdit(tempValue) diff --git a/frontend/src/pages/SettingsPage/UserSettingsForm.tsx b/frontend/src/pages/SettingsPage/UserSettingsForm.tsx index eb72923..cda56bc 100644 --- a/frontend/src/pages/SettingsPage/UserSettingsForm.tsx +++ b/frontend/src/pages/SettingsPage/UserSettingsForm.tsx @@ -1,89 +1,77 @@ -import React from "react"; -import { connect } from "react-redux"; -import { - Segment, - Message, - Form, - Button, - TextArea, - Ref, - Input, -} from "semantic-ui-react"; -import { useForm } from "react-hook-form"; -import { useTranslation } from "react-i18next"; +import React from 'react' +import {connect} from 'react-redux' +import {Segment, Message, Form, Button, TextArea, Ref, Input} from 'semantic-ui-react' +import {useForm} from 'react-hook-form' +import {useTranslation} from 'react-i18next' -import { setLogin } from "reducers/login"; -import api from "api"; -import { findInput } from "utils"; +import {setLogin} from 'reducers/login' +import api from 'api' +import {findInput} from 'utils' -const UserSettingsForm = connect((state) => ({ login: state.login }), { +const UserSettingsForm = connect((state) => ({login: state.login}), { setLogin, -})(function UserSettingsForm({ login, setLogin, errors, setErrors }) { - const { t } = useTranslation(); - const { register, handleSubmit } = useForm(); - const [loading, setLoading] = React.useState(false); +})(function UserSettingsForm({login, setLogin, errors, setErrors}) { + const {t} = useTranslation() + const {register, handleSubmit} = useForm() + const [loading, setLoading] = React.useState(false) const onSave = React.useCallback( async (changes) => { - setLoading(true); - setErrors(null); + setLoading(true) + setErrors(null) try { - const response = await api.put("/user", { body: changes }); - setLogin(response); + const response = await api.put('/user', {body: changes}) + setLogin(response) } catch (err) { - setErrors(err.errors); + setErrors(err.errors) } finally { - setLoading(false); + setLoading(false) } }, [setLoading, setLogin, setErrors] - ); + ) return ( - +
- + - {t("SettingsPage.profile.username.hint")} + {t('SettingsPage.profile.username.hint')} - {t("SettingsPage.profile.publicNotice")} + {t('SettingsPage.profile.publicNotice')} - + - + - {t("SettingsPage.profile.displayName.fallbackNotice")} + {t('SettingsPage.profile.displayName.fallbackNotice')} - +