diff --git a/frontend/src/components/Stats/index.tsx b/frontend/src/components/Stats/index.tsx
index 2c7abd3..ef0b3c2 100644
--- a/frontend/src/components/Stats/index.tsx
+++ b/frontend/src/components/Stats/index.tsx
@@ -1,118 +1,145 @@
-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 (
<>
- {user ? t('Stats.titleUser') : t('Stats.title')}
-
diff --git a/frontend/src/pages/SettingsPage.tsx b/frontend/src/pages/SettingsPage.tsx
deleted file mode 100644
index 11be6e5..0000000
--- a/frontend/src/pages/SettingsPage.tsx
+++ /dev/null
@@ -1,227 +0,0 @@
-import React from "react";
-import { connect } from "react-redux";
-import {
- Message,
- Icon,
- Grid,
- Form,
- Button,
- TextArea,
- Ref,
- Input,
- Header,
- Divider,
- Popup,
-} from "semantic-ui-react";
-import { useForm } from "react-hook-form";
-import Markdown from "react-markdown";
-import { useTranslation } from "react-i18next";
-
-import { setLogin } from "reducers/login";
-import { Page, Stats } from "components";
-import api from "api";
-import { findInput } from "utils";
-import { useConfig } from "config";
-
-const SettingsPage = connect((state) => ({ login: state.login }), { setLogin })(
- function SettingsPage({ login, setLogin }) {
- const { t } = useTranslation();
- const { register, handleSubmit } = useForm();
- const [loading, setLoading] = React.useState(false);
- const [errors, setErrors] = React.useState(null);
-
- const onSave = React.useCallback(
- async (changes) => {
- setLoading(true);
- setErrors(null);
- try {
- const response = await api.put("/user", { body: changes });
- setLogin(response);
- } catch (err) {
- setErrors(err.errors);
- } finally {
- setLoading(false);
- }
- },
- [setLoading, setLogin, setErrors]
- );
-
- const onGenerateNewKey = React.useCallback(async () => {
- setLoading(true);
- setErrors(null);
- try {
- const response = await api.put("/user", {
- body: { updateApiKey: true },
- });
- setLogin(response);
- } catch (err) {
- setErrors(err.errors);
- } finally {
- setLoading(false);
- }
- }, [setLoading, setLogin, setErrors]);
-
- return (
-
-
-
-
- {t("SettingsPage.profile.title")}
-
-
- {t("SettingsPage.profile.username.label")}
- [
- ]
-
- {t("SettingsPage.profile.username.hint")}
-
-
-
- {t("SettingsPage.profile.publicNotice")}
-
-
-
- {t("SettingsPage.profile.displayName.label")}
- [
- ]
-
-
- {t("SettingsPage.profile.displayName.fallbackNotice")}
-
-
-
-
- {t("SettingsPage.profile.bio.label")}
- [
-
- ]
-
-
- {t("SettingsPage.profile.avatarUrl.label")}
- [
- ]
-
-
-
-
- {t("general.save")}
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
- }
-);
-
-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);
- } catch (err) {
- setSuccess(false);
- } finally {
- setTimeout(() => {
- setSuccess(null);
- }, 2000);
- }
- };
-
- return (
-
- }
- position="top right"
- open={success != null}
- content={success ? t("general.copied") : t("general.copyError")}
- />
- );
-}
-
-const selectField = findInput((ref) => ref?.select());
-
-function ApiKeyDialog({ login, onGenerateNewKey }) {
- const { t } = useTranslation();
- const config = useConfig();
- const [show, setShow] = React.useState(false);
- const onClick = React.useCallback(
- (e) => {
- e.preventDefault();
- setShow(true);
- },
- [setShow]
- );
-
- const onGenerateNewKeyInner = React.useCallback(
- (e) => {
- e.preventDefault();
- onGenerateNewKey();
- },
- [onGenerateNewKey]
- );
-
- return (
- <>
-
{t("SettingsPage.apiKey.title")}
-
{t("SettingsPage.apiKey.description")}
-
- {show ? (
- login.apiKey ? (
- [
- ]
-
- ) : (
-
- )
- ) : (
-
- {t("SettingsPage.apiKey.key.show")}
-
- )}
-
-
{t("SettingsPage.apiKey.urlDescription")}
-
-
-
-
{t("SettingsPage.apiKey.generateDescription")}
-
-
- {t("SettingsPage.apiKey.generate")}
-
- >
- );
-}
-
-export default SettingsPage;
diff --git a/frontend/src/pages/SettingsPage/ApiKeySettings.tsx b/frontend/src/pages/SettingsPage/ApiKeySettings.tsx
new file mode 100644
index 0000000..b3f60ce
--- /dev/null
+++ b/frontend/src/pages/SettingsPage/ApiKeySettings.tsx
@@ -0,0 +1,125 @@
+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";
+
+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);
+ } catch (err) {
+ setSuccess(false);
+ } finally {
+ setTimeout(() => {
+ setSuccess(null);
+ }, 2000);
+ }
+ };
+
+ return (
+
+ }
+ position="top right"
+ open={success != null}
+ content={success ? t("general.copied") : t("general.copyError")}
+ />
+ );
+}
+
+const selectField = findInput((ref) => ref?.select());
+
+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);
+ const onClick = React.useCallback(
+ (e) => {
+ e.preventDefault();
+ setShow(true);
+ },
+ [setShow]
+ );
+
+ const onGenerateNewKey = React.useCallback(
+ async (e) => {
+ e.preventDefault();
+ setLoading(true);
+ try {
+ const response = await api.put("/user", {
+ body: { updateApiKey: true },
+ });
+ setLogin(response);
+ } catch (err) {
+ setErrors(err.errors);
+ } finally {
+ setLoading(false);
+ }
+ },
+ [setLoading, setLogin, setErrors]
+ );
+
+ return (
+
+ {t("SettingsPage.apiKey.description")}
+
+ {show ? (
+ login.apiKey ? (
+ [
+ ]
+
+ ) : (
+
+ )
+ ) : (
+
+ {t("SettingsPage.apiKey.key.show")}
+
+ )}
+
+ {t("SettingsPage.apiKey.urlDescription")}
+
+
+
+ {t("SettingsPage.apiKey.generateDescription")}
+
+
+ {t("SettingsPage.apiKey.generate")}
+
+
+ );
+});
+
+export default ApiKeySettings;
diff --git a/frontend/src/pages/SettingsPage/UserSettingsForm.tsx b/frontend/src/pages/SettingsPage/UserSettingsForm.tsx
new file mode 100644
index 0000000..eb72923
--- /dev/null
+++ b/frontend/src/pages/SettingsPage/UserSettingsForm.tsx
@@ -0,0 +1,89 @@
+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";
+
+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);
+
+ const onSave = React.useCallback(
+ async (changes) => {
+ setLoading(true);
+ setErrors(null);
+ try {
+ const response = await api.put("/user", { body: changes });
+ setLogin(response);
+ } catch (err) {
+ setErrors(err.errors);
+ } finally {
+ setLoading(false);
+ }
+ },
+ [setLoading, setLogin, setErrors]
+ );
+
+ return (
+
+
+ {t("SettingsPage.profile.username.label")}
+ [
+ ]
+
+ {t("SettingsPage.profile.username.hint")}
+
+
+
+ {t("SettingsPage.profile.publicNotice")}
+
+
+
+ {t("SettingsPage.profile.displayName.label")}
+ [
+ ]
+
+ {t("SettingsPage.profile.displayName.fallbackNotice")}
+
+
+
+ {t("SettingsPage.profile.bio.label")}
+ [
+
+ ]
+
+
+ {t("SettingsPage.profile.avatarUrl.label")}
+ [
+ ]
+
+
+
+
+ {t("general.save")}
+
+
+
+ );
+});
+export default UserSettingsForm;
diff --git a/frontend/src/pages/SettingsPage/index.tsx b/frontend/src/pages/SettingsPage/index.tsx
new file mode 100644
index 0000000..e67e931
--- /dev/null
+++ b/frontend/src/pages/SettingsPage/index.tsx
@@ -0,0 +1,64 @@
+import React from "react";
+import { connect } from "react-redux";
+import { Header, Tab } from "semantic-ui-react";
+import { useForm } from "react-hook-form";
+import { useTranslation } from "react-i18next";
+
+import { setLogin } from "reducers/login";
+import { Page, Stats } from "components";
+import api from "api";
+
+import ApiKeySettings from "./ApiKeySettings";
+
+import UserSettingsForm from "./UserSettingsForm";
+
+const SettingsPage = connect((state) => ({ login: state.login }), { setLogin })(
+ function SettingsPage({ login, setLogin }) {
+ const { t } = useTranslation();
+ const { register, handleSubmit } = useForm();
+ const [loading, setLoading] = React.useState(false);
+ const [errors, setErrors] = React.useState(null);
+
+ const onGenerateNewKey = React.useCallback(async () => {
+ setLoading(true);
+ setErrors(null);
+ try {
+ const response = await api.put("/user", {
+ body: { updateApiKey: true },
+ });
+ setLogin(response);
+ } catch (err) {
+ setErrors(err.errors);
+ } finally {
+ setLoading(false);
+ }
+ }, [setLoading, setLogin, setErrors]);
+
+ return (
+
+ {t("SettingsPage.title")}
+ ,
+ },
+
+ {
+ menuItem: t("SettingsPage.apiKey.title"),
+ render: () => ,
+ },
+
+ {
+ menuItem: t("SettingsPage.stats.title"),
+ render: () => ,
+ },
+ ]}
+ />
+
+ );
+ }
+);
+
+export default SettingsPage;
diff --git a/frontend/src/pages/UploadPage.tsx b/frontend/src/pages/UploadPage.tsx
index a76919c..ed92bd3 100644
--- a/frontend/src/pages/UploadPage.tsx
+++ b/frontend/src/pages/UploadPage.tsx
@@ -1,6 +1,6 @@
import _ from "lodash";
import React from "react";
-import { List, Loader, Table, Icon } from "semantic-ui-react";
+import { Header, List, Loader, Table, Icon } from "semantic-ui-react";
import { Link } from "react-router-dom";
import { useTranslation } from "react-i18next";
@@ -150,8 +150,10 @@ export default function UploadPage() {
const { t } = useTranslation();
+ const title = t("UploadPage.title");
return (
-
+
+
{files.length ? (
diff --git a/frontend/src/translations/de.yaml b/frontend/src/translations/de.yaml
index a0f42ff..40a6100 100644
--- a/frontend/src/translations/de.yaml
+++ b/frontend/src/translations/de.yaml
@@ -49,11 +49,10 @@ LoginButton:
login: Anmelden
HomePage:
+ stats: Statistik
mostRecentTrack: Neueste Fahrt
Stats:
- title: Statistik
- titleUser: Meine Statistik
placeholder: "..."
totalTrackLength: Gesamtfahrstrecke
timeRecorded: Aufzeichnungszeit
@@ -104,6 +103,7 @@ ExportPage:
label: Geografischer Bereich
UploadPage:
+ title: Fahrten hochladen
uploadProgress: Lade hoch {{progress}}%
processing: Verarbeiten...
@@ -212,10 +212,10 @@ MapPage:
eventCount: Anzahl Überholungen
SettingsPage:
- title: Einstellungen
+ title: Mein Konto
profile:
- title: Mein Profil
+ title: Profil
publicNotice: Alle Informationen ab hier sind öffentlich.
username:
label: Kontoname
@@ -231,7 +231,7 @@ SettingsPage:
label: Avatar URL
apiKey:
- title: Mein API-Schlüssel
+ title: API-Schlüssel
description: |
Hier findest du deinen API-Schlüssel für die Nutzung mit dem
OpenBikeSensor. Du kannst ihn dir herauskopieren und in der Seite für die
@@ -258,6 +258,9 @@ SettingsPage:
generate: Neuen API-Schlüssel erstellen
+ stats:
+ title: Statistik
+
TrackPage:
downloadFailed: Download fehlgeschlagen
downloadError: Diese Fahrt wurde vermutlich nicht korrekt importiert, oder in letzter Zeit nicht aktualisiert. Bitte frage den Administrator um Hilfe mit diesem Problem.
diff --git a/frontend/src/translations/en.yaml b/frontend/src/translations/en.yaml
index 14de675..68b00e8 100644
--- a/frontend/src/translations/en.yaml
+++ b/frontend/src/translations/en.yaml
@@ -54,11 +54,10 @@ LoginButton:
login: Login
HomePage:
+ stats: Statistics
mostRecentTrack: Most recent track
Stats:
- title: Statistics
- titleUser: My Statistic
placeholder: "..."
totalTrackLength: Total track length
timeRecorded: Time recorded
@@ -110,6 +109,7 @@ ExportPage:
label: Bounding Box
UploadPage:
+ title: Upload tracks
uploadProgress: Uploading {{progress}}%
processing: Processing...
@@ -217,10 +217,10 @@ MapPage:
eventCount: Event count
SettingsPage:
- title: Settings
+ title: My Account
profile:
- title: My profile
+ title: Profile
publicNotice: All of the information below is public.
username:
label: Username
@@ -236,7 +236,7 @@ SettingsPage:
label: Avatar URL
apiKey:
- title: My API Key
+ title: API Key
description: |
Here you find your API Key, for use in the OpenBikeSensor. You can to
copy and paste it into your sensor's configuration interface to allow
@@ -260,6 +260,8 @@ SettingsPage:
generate: Generate new API key
+ stats:
+ title: Statistics
TrackPage:
downloadFailed: Download failed