Translate SettingsPage
This commit is contained in:
parent
76943fb1f0
commit
fe7d7ce274
|
@ -1,87 +1,105 @@
|
||||||
import React from 'react'
|
import React from "react";
|
||||||
import {connect} from 'react-redux'
|
import { connect } from "react-redux";
|
||||||
import {Message, Icon, Grid, Form, Button, TextArea, Ref, Input, Header, Divider, Popup} from 'semantic-ui-react'
|
import {
|
||||||
import {useForm} from 'react-hook-form'
|
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 { setLogin } from "reducers/login";
|
||||||
import {Page, Stats} from 'components'
|
import { Page, Stats } from "components";
|
||||||
import api from 'api'
|
import api from "api";
|
||||||
import {findInput} from 'utils'
|
import { findInput } from "utils";
|
||||||
import {useConfig} from 'config'
|
import { useConfig } from "config";
|
||||||
|
|
||||||
const SettingsPage = connect((state) => ({login: state.login}), {setLogin})(function SettingsPage({login, setLogin}) {
|
const SettingsPage = connect((state) => ({ login: state.login }), { setLogin })(
|
||||||
const {register, handleSubmit} = useForm()
|
function SettingsPage({ login, setLogin }) {
|
||||||
const [loading, setLoading] = React.useState(false)
|
const { t } = useTranslation();
|
||||||
const [errors, setErrors] = React.useState(null)
|
const { register, handleSubmit } = useForm();
|
||||||
|
const [loading, setLoading] = React.useState(false);
|
||||||
|
const [errors, setErrors] = React.useState(null);
|
||||||
|
|
||||||
const onSave = React.useCallback(
|
const onSave = React.useCallback(
|
||||||
async (changes) => {
|
async (changes) => {
|
||||||
setLoading(true)
|
setLoading(true);
|
||||||
setErrors(null)
|
setErrors(null);
|
||||||
try {
|
try {
|
||||||
const response = await api.put('/user', {body: changes})
|
const response = await api.put("/user", { body: changes });
|
||||||
setLogin(response)
|
setLogin(response);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setErrors(err.errors)
|
setErrors(err.errors);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[setLoading, setLogin, setErrors]
|
[setLoading, setLogin, setErrors]
|
||||||
)
|
);
|
||||||
|
|
||||||
const onGenerateNewKey = React.useCallback(async () => {
|
const onGenerateNewKey = React.useCallback(async () => {
|
||||||
setLoading(true)
|
setLoading(true);
|
||||||
setErrors(null)
|
setErrors(null);
|
||||||
try {
|
try {
|
||||||
const response = await api.put('/user', {body: {updateApiKey: true}})
|
const response = await api.put("/user", {
|
||||||
setLogin(response)
|
body: { updateApiKey: true },
|
||||||
|
});
|
||||||
|
setLogin(response);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setErrors(err.errors)
|
setErrors(err.errors);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, [setLoading, setLogin, setErrors])
|
}, [setLoading, setLogin, setErrors]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page title="Settings">
|
<Page title={t("SettingsPage.title")}>
|
||||||
<Grid centered relaxed divided stackable>
|
<Grid centered relaxed divided stackable>
|
||||||
<Grid.Row>
|
<Grid.Row>
|
||||||
<Grid.Column width={8}>
|
<Grid.Column width={8}>
|
||||||
<Header as="h2">My profile</Header>
|
<Header as="h2">{t("SettingsPage.profile.title")}</Header>
|
||||||
|
|
||||||
<Message info>All of this information is public.</Message>
|
<Message info>{t("SettingsPage.profile.publicNotice")}</Message>
|
||||||
|
|
||||||
<Form onSubmit={handleSubmit(onSave)} loading={loading}>
|
<Form onSubmit={handleSubmit(onSave)} loading={loading}>
|
||||||
<Ref innerRef={findInput(register)}>
|
<Ref innerRef={findInput(register)}>
|
||||||
<Form.Input
|
<Form.Input
|
||||||
error={errors?.username}
|
error={errors?.username}
|
||||||
label="Username"
|
label={t("SettingsPage.profile.username.label")}
|
||||||
name="username"
|
name="username"
|
||||||
defaultValue={login.username}
|
defaultValue={login.username}
|
||||||
disabled
|
disabled
|
||||||
/>
|
/>
|
||||||
</Ref>
|
</Ref>
|
||||||
<Form.Field error={errors?.bio}>
|
<Form.Field error={errors?.bio}>
|
||||||
<label>Bio</label>
|
<label>{t("SettingsPage.profile.bio.label")}</label>
|
||||||
<Ref innerRef={register}>
|
<Ref innerRef={register}>
|
||||||
<TextArea name="bio" rows={4} defaultValue={login.bio} />
|
<TextArea name="bio" rows={4} defaultValue={login.bio} />
|
||||||
</Ref>
|
</Ref>
|
||||||
</Form.Field>
|
</Form.Field>
|
||||||
<Form.Field error={errors?.image}>
|
<Form.Field error={errors?.image}>
|
||||||
<label>Avatar URL</label>
|
<label>{t("SettingsPage.profile.avatarUrl.label")}</label>
|
||||||
<Ref innerRef={findInput(register)}>
|
<Ref innerRef={findInput(register)}>
|
||||||
<Input name="image" defaultValue={login.image} />
|
<Input name="image" defaultValue={login.image} />
|
||||||
</Ref>
|
</Ref>
|
||||||
</Form.Field>
|
</Form.Field>
|
||||||
|
|
||||||
<Button type="submit" primary>
|
<Button type="submit" primary>
|
||||||
Save
|
{t("general.save")}
|
||||||
</Button>
|
</Button>
|
||||||
</Form>
|
</Form>
|
||||||
</Grid.Column>
|
</Grid.Column>
|
||||||
<Grid.Column width={6}>
|
<Grid.Column width={6}>
|
||||||
<ApiKeyDialog {...{login, onGenerateNewKey}} />
|
<ApiKeyDialog {...{ login, onGenerateNewKey }} />
|
||||||
|
|
||||||
<Divider />
|
<Divider />
|
||||||
|
|
||||||
|
@ -90,89 +108,101 @@ const SettingsPage = connect((state) => ({login: state.login}), {setLogin})(func
|
||||||
</Grid.Row>
|
</Grid.Row>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Page>
|
</Page>
|
||||||
)
|
);
|
||||||
})
|
}
|
||||||
|
);
|
||||||
|
|
||||||
function CopyInput({value, ...props}) {
|
function CopyInput({ value, ...props }) {
|
||||||
const [success, setSuccess] = React.useState(null)
|
const { t } = useTranslation();
|
||||||
|
const [success, setSuccess] = React.useState(null);
|
||||||
const onClick = async () => {
|
const onClick = async () => {
|
||||||
try {
|
try {
|
||||||
await window.navigator?.clipboard?.writeText(value)
|
await window.navigator?.clipboard?.writeText(value);
|
||||||
setSuccess(true)
|
setSuccess(true);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setSuccess(false)
|
setSuccess(false);
|
||||||
} finally {
|
} finally {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setSuccess(null)
|
setSuccess(null);
|
||||||
}, 2000)
|
}, 2000);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popup
|
<Popup
|
||||||
trigger={<Input {...props} value={value} fluid action={{icon: 'copy', onClick}} />}
|
trigger={
|
||||||
|
<Input
|
||||||
|
{...props}
|
||||||
|
value={value}
|
||||||
|
fluid
|
||||||
|
action={{ icon: "copy", onClick }}
|
||||||
|
/>
|
||||||
|
}
|
||||||
position="top right"
|
position="top right"
|
||||||
open={success != null}
|
open={success != null}
|
||||||
content={success ? 'Copied.' : 'Failed to copy.'}
|
content={success ? t('general.copied') : t('general.copyError')}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectField = findInput((ref) => ref?.select())
|
const selectField = findInput((ref) => ref?.select());
|
||||||
|
|
||||||
function ApiKeyDialog({login, onGenerateNewKey}) {
|
function ApiKeyDialog({ login, onGenerateNewKey }) {
|
||||||
const config = useConfig()
|
const { t } = useTranslation();
|
||||||
const [show, setShow] = React.useState(false)
|
const config = useConfig();
|
||||||
|
const [show, setShow] = React.useState(false);
|
||||||
const onClick = React.useCallback(
|
const onClick = React.useCallback(
|
||||||
(e) => {
|
(e) => {
|
||||||
e.preventDefault()
|
e.preventDefault();
|
||||||
setShow(true)
|
setShow(true);
|
||||||
},
|
},
|
||||||
[setShow]
|
[setShow]
|
||||||
)
|
);
|
||||||
|
|
||||||
const onGenerateNewKeyInner = React.useCallback(
|
const onGenerateNewKeyInner = React.useCallback(
|
||||||
(e) => {
|
(e) => {
|
||||||
e.preventDefault()
|
e.preventDefault();
|
||||||
onGenerateNewKey()
|
onGenerateNewKey();
|
||||||
},
|
},
|
||||||
[onGenerateNewKey]
|
[onGenerateNewKey]
|
||||||
)
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Header as="h2">My API Key</Header>
|
<Header as="h2">{t("SettingsPage.apiKey.title")}</Header>
|
||||||
<p>
|
<Markdown>{t("SettingsPage.apiKey.description")}</Markdown>
|
||||||
Here you find your API Key, for use in the OpenBikeSensor. You can to copy and paste it into your sensor's
|
<div style={{ minHeight: 40, marginBottom: 16 }}>
|
||||||
configuration interface to allow direct upload from the device.
|
|
||||||
</p>
|
|
||||||
<p>Please protect your API Key carefully as it allows full control over your account.</p>
|
|
||||||
<div style={{minHeight: 40, marginBottom: 16}}>
|
|
||||||
{show ? (
|
{show ? (
|
||||||
login.apiKey ? (
|
login.apiKey ? (
|
||||||
<Ref innerRef={selectField}>
|
<Ref innerRef={selectField}>
|
||||||
<CopyInput label="Personal API Key" value={login.apiKey} />
|
<CopyInput
|
||||||
|
label={t("SettingsPage.apiKey.key.label")}
|
||||||
|
value={login.apiKey}
|
||||||
|
/>
|
||||||
</Ref>
|
</Ref>
|
||||||
) : (
|
) : (
|
||||||
<Message warning content="You have no API Key, please generate one below." />
|
<Message warning content={t("SettingsPage.apiKey.key.empty")} />
|
||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
<Button onClick={onClick}>
|
<Button onClick={onClick}>
|
||||||
<Icon name="lock" /> Show API Key
|
<Icon name="lock" /> {t("SettingsPage.apiKey.key.show")}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<p>The API URL should be set to:</p>
|
<Markdown>{t("SettingsPage.apiKey.urlDescription")}</Markdown>
|
||||||
<div style={{marginBottom: 16}}>
|
<div style={{ marginBottom: 16 }}>
|
||||||
<CopyInput label="API URL" value={config?.apiUrl?.replace(/\/api$/, '') ?? '...'} />
|
<CopyInput
|
||||||
|
label={t("SettingsPage.apiKey.url.label")}
|
||||||
|
value={config?.apiUrl?.replace(/\/api$/, "") ?? "..."}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p>
|
<Markdown>{t("SettingsPage.apiKey.generateDescription")}</Markdown>
|
||||||
You can generate a new API Key here, which will invalidate the old one, disconnecting all devices you used it on
|
<p></p>
|
||||||
from your account.
|
<Button onClick={onGenerateNewKeyInner}>
|
||||||
</p>
|
{t("SettingsPage.apiKey.generate")}
|
||||||
<Button onClick={onGenerateNewKeyInner}>Generate new API key</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SettingsPage
|
export default SettingsPage;
|
||||||
|
|
|
@ -179,3 +179,45 @@ MapPage:
|
||||||
southWest: südwestwärts
|
southWest: südwestwärts
|
||||||
west: westwärts
|
west: westwärts
|
||||||
northWest: nordwestwärts
|
northWest: nordwestwärts
|
||||||
|
|
||||||
|
SettingsPage:
|
||||||
|
title: Einstellungen
|
||||||
|
|
||||||
|
profile:
|
||||||
|
title: Mein Profil
|
||||||
|
publicNotice: All diese Informationen sind öffentlich.
|
||||||
|
username:
|
||||||
|
label: Kontoname
|
||||||
|
bio:
|
||||||
|
label: Bio
|
||||||
|
avatarUrl:
|
||||||
|
label: Avatar URL
|
||||||
|
|
||||||
|
apiKey:
|
||||||
|
title: Mein 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
|
||||||
|
Einstellungen deines Geräts einfügen, um dann direkt vom Gerät aus
|
||||||
|
Fahrten hochladen zu können.
|
||||||
|
|
||||||
|
Bitte schütze deinen API-Schlüssel vor ungewolltem Zugriff, denn er
|
||||||
|
erlaubt Zugang zu deinem Konto auf dieser Seite.
|
||||||
|
urlDescription: |
|
||||||
|
Die API-URL sollte wie folgt gesetzt werden:
|
||||||
|
|
||||||
|
generateDescription: |
|
||||||
|
Hier kannst du einen neuen API-Schlüssel für dein Konto erstellen. Der
|
||||||
|
alte wird damit ungültig, und alle Geräte, die damit konfiguriert wurden,
|
||||||
|
können nicht mehr auf dein Konto zugreifen.
|
||||||
|
|
||||||
|
key:
|
||||||
|
label: Persönlicher API-Schlüssel
|
||||||
|
empty: Du hast keinen API-Schlüssel, kannst dir aber unten einen erstellen.
|
||||||
|
show: API-Schlüssel zeigen
|
||||||
|
|
||||||
|
url:
|
||||||
|
label: API URL
|
||||||
|
|
||||||
|
generate: Neuen API-Schlüssel erstellen
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,10 @@ general:
|
||||||
private: Private
|
private: Private
|
||||||
show: Show
|
show: Show
|
||||||
edit: Edit
|
edit: Edit
|
||||||
|
save: Save
|
||||||
|
|
||||||
|
copied: Copied.
|
||||||
|
copyError: Failed to copy.
|
||||||
|
|
||||||
App:
|
App:
|
||||||
footer:
|
footer:
|
||||||
|
@ -183,3 +187,42 @@ MapPage:
|
||||||
southWest: south-west bound
|
southWest: south-west bound
|
||||||
west: west bound
|
west: west bound
|
||||||
northWest: north-west bound
|
northWest: north-west bound
|
||||||
|
|
||||||
|
SettingsPage:
|
||||||
|
title: Settings
|
||||||
|
|
||||||
|
profile:
|
||||||
|
title: My profile
|
||||||
|
publicNotice: All of this information is public.
|
||||||
|
username:
|
||||||
|
label: Username
|
||||||
|
bio:
|
||||||
|
label: Bio
|
||||||
|
avatarUrl:
|
||||||
|
label: Avatar URL
|
||||||
|
|
||||||
|
apiKey:
|
||||||
|
title: My 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
|
||||||
|
direct upload from the device.
|
||||||
|
|
||||||
|
Please protect your API Key carefully as it allows full control over
|
||||||
|
your account.
|
||||||
|
urlDescription: |
|
||||||
|
The API URL should be set to:
|
||||||
|
generateDescription: |
|
||||||
|
You can generate a new API Key here, which will invalidate the old one,
|
||||||
|
disconnecting all devices you used it on from your account.
|
||||||
|
|
||||||
|
key:
|
||||||
|
label: Personal API Key
|
||||||
|
empty: You have no API Key, please generate one below.
|
||||||
|
show: Show API Key
|
||||||
|
|
||||||
|
url:
|
||||||
|
label: API URL
|
||||||
|
|
||||||
|
generate: Generate new API key
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue