Translate TrackEditor
This commit is contained in:
parent
6f7c8d54f2
commit
f158414f24
|
@ -1,6 +1,6 @@
|
||||||
import React from 'react'
|
import React from "react";
|
||||||
import _ from 'lodash'
|
import _ from "lodash";
|
||||||
import {connect} from 'react-redux'
|
import { connect } from "react-redux";
|
||||||
import {
|
import {
|
||||||
Divider,
|
Divider,
|
||||||
Message,
|
Message,
|
||||||
|
@ -14,189 +14,201 @@ import {
|
||||||
TextArea,
|
TextArea,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
Header,
|
Header,
|
||||||
} from 'semantic-ui-react'
|
} from "semantic-ui-react";
|
||||||
import {useHistory, useParams, Link} from 'react-router-dom'
|
import { useHistory, useParams, Link } from "react-router-dom";
|
||||||
import {concat, of, from} from 'rxjs'
|
import { concat, of, from } from "rxjs";
|
||||||
import {pluck, distinctUntilChanged, map, switchMap} from 'rxjs/operators'
|
import { pluck, distinctUntilChanged, map, switchMap } from "rxjs/operators";
|
||||||
import {useObservable} from 'rxjs-hooks'
|
import { useObservable } from "rxjs-hooks";
|
||||||
import {findInput} from 'utils'
|
import { findInput } from "utils";
|
||||||
import {useForm, Controller} from 'react-hook-form'
|
import { useForm, Controller } from "react-hook-form";
|
||||||
|
import { useTranslation, Trans as Translate } from "react-i18next";
|
||||||
|
import Markdown from "react-markdown";
|
||||||
|
|
||||||
import api from 'api'
|
import api from "api";
|
||||||
import {Page, FileUploadField} from 'components'
|
import { Page, FileUploadField } from "components";
|
||||||
import type {Track} from 'types'
|
import type { Track } from "types";
|
||||||
|
|
||||||
import {FileUploadStatus} from 'pages/UploadPage'
|
import { FileUploadStatus } from "pages/UploadPage";
|
||||||
|
|
||||||
function ReplaceTrackData({slug}) {
|
function ReplaceTrackData({ slug }) {
|
||||||
const [file, setFile] = React.useState(null)
|
const { t } = useTranslation();
|
||||||
const [result, setResult] = React.useState(null)
|
const [file, setFile] = React.useState(null);
|
||||||
const onComplete = React.useCallback((_id, r) => setResult(r), [setResult])
|
const [result, setResult] = React.useState(null);
|
||||||
|
const onComplete = React.useCallback((_id, r) => setResult(r), [setResult]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Header as="h2">Replace track data</Header>
|
<Header as="h2">{t("TrackEditor.replaceTrackData")}</Header>
|
||||||
{!file ? (
|
{!file ? (
|
||||||
<FileUploadField onSelect={setFile} />
|
<FileUploadField onSelect={setFile} />
|
||||||
) : result ? (
|
) : result ? (
|
||||||
<Message>
|
<Message>
|
||||||
Upload complete. <Link to={`/tracks/${slug}`}>Show track</Link>
|
<Translate i18nKey="TrackEditor.uploadComplete">
|
||||||
|
Upload complete. <Link to={`/tracks/${slug}`}>Show track</Link>
|
||||||
|
</Translate>
|
||||||
</Message>
|
</Message>
|
||||||
) : (
|
) : (
|
||||||
<FileUploadStatus {...{file, onComplete, slug}} />
|
<FileUploadStatus {...{ file, onComplete, slug }} />
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const TrackEditor = connect((state) => ({login: state.login}))(function TrackEditor({login}) {
|
const TrackEditor = connect((state) => ({ login: state.login }))(
|
||||||
const [busy, setBusy] = React.useState(false)
|
function TrackEditor({ login }) {
|
||||||
const {register, control, handleSubmit} = useForm()
|
const { t } = useTranslation();
|
||||||
const {slug} = useParams()
|
const [busy, setBusy] = React.useState(false);
|
||||||
const history = useHistory()
|
const { register, control, handleSubmit } = useForm();
|
||||||
|
const { slug } = useParams();
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
const track: null | Track = useObservable(
|
const track: null | Track = useObservable(
|
||||||
(_$, args$) => {
|
(_$, args$) => {
|
||||||
const slug$ = args$.pipe(pluck(0), distinctUntilChanged())
|
const slug$ = args$.pipe(pluck(0), distinctUntilChanged());
|
||||||
return slug$.pipe(
|
return slug$.pipe(
|
||||||
map((slug) => `/tracks/${slug}`),
|
map((slug) => `/tracks/${slug}`),
|
||||||
switchMap((url) => concat(of(null), from(api.get(url)))),
|
switchMap((url) => concat(of(null), from(api.get(url)))),
|
||||||
pluck('track')
|
pluck("track")
|
||||||
)
|
);
|
||||||
},
|
},
|
||||||
null,
|
null,
|
||||||
[slug]
|
[slug]
|
||||||
)
|
);
|
||||||
|
|
||||||
const loading = busy || track == null
|
const loading = busy || track == null;
|
||||||
const isAuthor = login?.username === track?.author?.username
|
const isAuthor = login?.username === track?.author?.username;
|
||||||
|
|
||||||
// Navigate to track detials if we are not the author
|
// Navigate to track detials if we are not the author
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!login || (track && !isAuthor)) {
|
if (!login || (track && !isAuthor)) {
|
||||||
history.replace(`/tracks/${slug}`)
|
history.replace(`/tracks/${slug}`);
|
||||||
}
|
}
|
||||||
}, [slug, login, track, isAuthor, history])
|
}, [slug, login, track, isAuthor, history]);
|
||||||
|
|
||||||
const onSubmit = React.useMemo(
|
const onSubmit = React.useMemo(
|
||||||
() =>
|
() =>
|
||||||
handleSubmit(async (values) => {
|
handleSubmit(async (values) => {
|
||||||
setBusy(true)
|
setBusy(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await api.put(`/tracks/${slug}`, {body: {track: _.pickBy(values, (v) => typeof v !== 'undefined')}})
|
await api.put(`/tracks/${slug}`, {
|
||||||
history.push(`/tracks/${slug}`)
|
body: {
|
||||||
} finally {
|
track: _.pickBy(values, (v) => typeof v !== "undefined"),
|
||||||
setBusy(false)
|
},
|
||||||
}
|
});
|
||||||
}),
|
history.push(`/tracks/${slug}`);
|
||||||
[slug, handleSubmit, history]
|
} finally {
|
||||||
)
|
setBusy(false);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
[slug, handleSubmit, history]
|
||||||
|
);
|
||||||
|
|
||||||
const [confirmDelete, setConfirmDelete] = React.useState(false)
|
const [confirmDelete, setConfirmDelete] = React.useState(false);
|
||||||
const onDelete = React.useCallback(async () => {
|
const onDelete = React.useCallback(async () => {
|
||||||
setBusy(true)
|
setBusy(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await api.delete(`/tracks/${slug}`)
|
await api.delete(`/tracks/${slug}`);
|
||||||
history.push('/tracks')
|
history.push("/tracks");
|
||||||
} finally {
|
} finally {
|
||||||
setConfirmDelete(false)
|
setConfirmDelete(false);
|
||||||
setBusy(false)
|
setBusy(false);
|
||||||
}
|
}
|
||||||
}, [setBusy, setConfirmDelete, slug, history])
|
}, [setBusy, setConfirmDelete, slug, history]);
|
||||||
|
|
||||||
const title = `Edit ${track ? track.title || 'Unnamed track' : 'track'}`
|
const trackTitle: string = track?.title || t("general.unnamedTrack");
|
||||||
return (
|
const title = t("TrackEditor.title", { trackTitle });
|
||||||
<Page title={title}>
|
|
||||||
<Grid centered relaxed divided stackable>
|
|
||||||
<Grid.Row>
|
|
||||||
<Grid.Column width={10}>
|
|
||||||
<Header as="h2">{title}</Header>
|
|
||||||
<Form loading={loading} key={track?.slug} onSubmit={onSubmit}>
|
|
||||||
<Ref innerRef={findInput(register)}>
|
|
||||||
<Form.Input label="Title" name="title" defaultValue={track?.title} style={{fontSize: '120%'}} />
|
|
||||||
</Ref>
|
|
||||||
|
|
||||||
<Form.Field>
|
return (
|
||||||
<label>Description</label>
|
<Page title={title}>
|
||||||
<Ref innerRef={register}>
|
<Grid centered relaxed divided stackable>
|
||||||
<TextArea name="description" rows={4} defaultValue={track?.description} />
|
<Grid.Row>
|
||||||
|
<Grid.Column width={10}>
|
||||||
|
<Header as="h2">{title}</Header>
|
||||||
|
<Form loading={loading} key={track?.slug} onSubmit={onSubmit}>
|
||||||
|
<Ref innerRef={findInput(register)}>
|
||||||
|
<Form.Input
|
||||||
|
label="Title"
|
||||||
|
name="title"
|
||||||
|
defaultValue={track?.title}
|
||||||
|
style={{ fontSize: "120%" }}
|
||||||
|
/>
|
||||||
</Ref>
|
</Ref>
|
||||||
</Form.Field>
|
|
||||||
|
|
||||||
<Form.Field>
|
<Form.Field>
|
||||||
<label>Track visibility</label>
|
<label>{t("TrackEditor.description.label")}</label>
|
||||||
<Controller
|
<Ref innerRef={register}>
|
||||||
name="public"
|
<TextArea
|
||||||
control={control}
|
name="description"
|
||||||
defaultValue={track?.public}
|
rows={4}
|
||||||
render={(props) => (
|
defaultValue={track?.description}
|
||||||
<Checkbox
|
|
||||||
name="public"
|
|
||||||
label="Make track public (in track list and details page, events always visible anonymously)"
|
|
||||||
checked={props.value}
|
|
||||||
onChange={(_, {checked}) => props.onChange(checked)}
|
|
||||||
/>
|
/>
|
||||||
)}
|
</Ref>
|
||||||
/>
|
</Form.Field>
|
||||||
|
|
||||||
<Popup
|
<Form.Field>
|
||||||
wide="very"
|
<label>
|
||||||
content={
|
{t("TrackEditor.visibility.label")}
|
||||||
<>
|
<Popup
|
||||||
<p>
|
wide="very"
|
||||||
The overtaking events from all tracks are always public in an anonymized form.
|
content={
|
||||||
This option is about publishing the FULL TRACK (where you were cycling) for
|
<Markdown>
|
||||||
everyone to see together with your username.
|
{t("TrackEditor.visibility.description")}
|
||||||
</p>
|
</Markdown>
|
||||||
<p>
|
}
|
||||||
Checking this box allows all users to see your full track. For your own privacy and security,
|
trigger={
|
||||||
make sure to only publish tracks in this way that do not let others deduce where you live, work,
|
<Icon
|
||||||
or frequently stay. Your recording device might have useful privacy settings to not record
|
name="warning sign"
|
||||||
geolocation data near those places.
|
style={{ marginLeft: 8 }}
|
||||||
</p>
|
color="orange"
|
||||||
<p>
|
/>
|
||||||
In the future, this site will allow you to redact privacy sensitive data in tracks, both
|
}
|
||||||
manually and automatically. Until then, you will have to rely on the features of your recording
|
/>
|
||||||
device, or manually redact your files before upload.
|
</label>
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
After checking this box, your data essentially becomes public. You understand that we cannot
|
|
||||||
control who potentially downloads this data and and keeps a copy, even if you delete it from
|
|
||||||
your account or anonymize it later.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<b>Use at your own risk.</b>
|
|
||||||
</p>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
trigger={<Icon name="warning sign" style={{marginLeft: 8}} color="orange" />}
|
|
||||||
/>
|
|
||||||
</Form.Field>
|
|
||||||
<Button type="submit">Save</Button>
|
|
||||||
</Form>
|
|
||||||
</Grid.Column>
|
|
||||||
<Grid.Column width={6}>
|
|
||||||
<ReplaceTrackData slug={slug} />
|
|
||||||
|
|
||||||
<Divider />
|
<Controller
|
||||||
|
name="public"
|
||||||
|
control={control}
|
||||||
|
defaultValue={track?.public}
|
||||||
|
render={(props) => (
|
||||||
|
<Checkbox
|
||||||
|
name="public"
|
||||||
|
label={t("TrackEditor.visibility.checkboxLabel")}
|
||||||
|
checked={props.value}
|
||||||
|
onChange={(_, { checked }) => props.onChange(checked)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Form.Field>
|
||||||
|
<Button type="submit">{t("general.save")}</Button>
|
||||||
|
</Form>
|
||||||
|
</Grid.Column>
|
||||||
|
<Grid.Column width={6}>
|
||||||
|
<ReplaceTrackData slug={slug} />
|
||||||
|
|
||||||
<Header as="h2">Danger zone</Header>
|
<Divider />
|
||||||
<p>
|
|
||||||
You can remove this track from your account and the portal if you like. However, if at any point you have
|
|
||||||
published this track, we cannot guarantee that there are no versions of it in the public data repository,
|
|
||||||
or any copy thereof.
|
|
||||||
</p>
|
|
||||||
<Button color="red" onClick={() => setConfirmDelete(true)}>
|
|
||||||
Delete
|
|
||||||
</Button>
|
|
||||||
<Confirm open={confirmDelete} onCancel={() => setConfirmDelete(false)} onConfirm={onDelete} />
|
|
||||||
</Grid.Column>
|
|
||||||
</Grid.Row>
|
|
||||||
</Grid>
|
|
||||||
</Page>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
export default TrackEditor
|
<Header as="h2">{t("TrackEditor.dangerZone.title")}</Header>
|
||||||
|
<Markdown>{t("TrackEditor.dangerZone.description")}</Markdown>
|
||||||
|
|
||||||
|
<Button color="red" onClick={() => setConfirmDelete(true)}>
|
||||||
|
{t("general.delete")}
|
||||||
|
</Button>
|
||||||
|
<Confirm
|
||||||
|
open={confirmDelete}
|
||||||
|
onCancel={() => setConfirmDelete(false)}
|
||||||
|
onConfirm={onDelete}
|
||||||
|
content={t("TrackEditor.dangerZone.confirmDelete")}
|
||||||
|
confirmButton={t("general.delete")}
|
||||||
|
cancelButton={t("general.cancel")}
|
||||||
|
/>
|
||||||
|
</Grid.Column>
|
||||||
|
</Grid.Row>
|
||||||
|
</Grid>
|
||||||
|
</Page>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export default TrackEditor;
|
||||||
|
|
|
@ -355,6 +355,7 @@ const TrackPage = connect((state) => ({ login: state.login }))(
|
||||||
onConfirm={hideDownloadError}
|
onConfirm={hideDownloadError}
|
||||||
header={t("TrackPage.downloadFailed")}
|
header={t("TrackPage.downloadFailed")}
|
||||||
content={String(downloadError)}
|
content={String(downloadError)}
|
||||||
|
confirmButton={t('general.ok')}
|
||||||
/>
|
/>
|
||||||
</Page>
|
</Page>
|
||||||
);
|
);
|
||||||
|
|
|
@ -9,7 +9,7 @@ import _ from 'lodash'
|
||||||
import {useTranslation, Trans as Translate} from 'react-i18next'
|
import {useTranslation, Trans as Translate} from 'react-i18next'
|
||||||
|
|
||||||
import type {Track} from 'types'
|
import type {Track} from 'types'
|
||||||
import {Avatar, Page, StripMarkdown, FormattedDate} from 'components'
|
import {Avatar, Page, StripMarkdown, FormattedDate, Visibility} from 'components'
|
||||||
import api from 'api'
|
import api from 'api'
|
||||||
import {useQueryParam} from 'query'
|
import {useQueryParam} from 'query'
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,11 @@ general:
|
||||||
edit: Bearbeiten
|
edit: Bearbeiten
|
||||||
save: Speichern
|
save: Speichern
|
||||||
delete: Löschen
|
delete: Löschen
|
||||||
|
ok: Okay
|
||||||
|
cancel: Abbrechen
|
||||||
|
confirm: Bist du sicher?
|
||||||
|
copied: Kopiert
|
||||||
|
copyError: Kopieren fehlgeschlagen
|
||||||
|
|
||||||
App:
|
App:
|
||||||
footer:
|
footer:
|
||||||
|
@ -269,3 +274,50 @@ TrackPage:
|
||||||
title: Kommentare
|
title: Kommentare
|
||||||
post: Kommentar abschicken
|
post: Kommentar abschicken
|
||||||
empty: Bisher hat niemand diese Fahrt kommentiert.
|
empty: Bisher hat niemand diese Fahrt kommentiert.
|
||||||
|
|
||||||
|
TrackEditor:
|
||||||
|
title: "{{trackTitle}} bearbeiten"
|
||||||
|
replaceTrackData: Fahrtdaten ersetzen
|
||||||
|
uploadComplete: Upload vollständig. <1>Fahrt anzeigen</1>
|
||||||
|
|
||||||
|
dangerZone:
|
||||||
|
title: Gefahrenbereich
|
||||||
|
description: |
|
||||||
|
Du kannst diese Fahrt aus deinem Konto und damit von diesem Portal
|
||||||
|
entfernen. Wir können jedoch nicht garantieren, dass die daraus
|
||||||
|
extrahierten, anonymisierten und u. U. weiter veröffentlichten Daten
|
||||||
|
ebenfalls entfernt werden, weder in dieser Plattform, noch in Kopien
|
||||||
|
anderer Parteien.
|
||||||
|
confirmDelete: Bist du sicher, dass du diese Fahrt löschen möchtest?
|
||||||
|
|
||||||
|
description:
|
||||||
|
label: Beschreibung
|
||||||
|
visibility:
|
||||||
|
label: Sichtbarkeit der Fahrt
|
||||||
|
checkboxLabel: Fahrt öffentlich machen (in der Fahrtenliste und Detailseite, anonyme Überholungen sind immer sichtbar)
|
||||||
|
description: |
|
||||||
|
Die extrahierten Überholungen aller Fahrten sind immer öffentlich in
|
||||||
|
anonymisierter Form. Diese Option betrifft die Veröffentlichung der
|
||||||
|
*gesamten* Fahrtdaten, also wann und wo du gefahren bist, zusammen mit
|
||||||
|
deinem erkennbaren Kontonamen.
|
||||||
|
|
||||||
|
Wenn du diese Box aktivierst können alle deine ganze Fahrt ansehen. Um
|
||||||
|
deine eigene Sicherheit und Privatsphäre zu schützen stelle nur solche
|
||||||
|
Fahrten öffentlich, anhand derer niemand deinen Wohnort, Arbeitsplatz,
|
||||||
|
oder regelmäßige Aufenthaltsorte erkennen kann. Dein Aufnahmegerät stellt
|
||||||
|
eventuell nützliche Einstellungen zur Verfügung, um die GPS-Daten in der
|
||||||
|
Nähe solcher Orte überhaupt nicht aufzuzeichnen.
|
||||||
|
|
||||||
|
In der Zukunft wird diese Seite auch Werkzeuge zur Redaktion deiner
|
||||||
|
sensiblen Daten, sowohl manuell als auch automatisch, bereitstellen. Bis
|
||||||
|
dahin musst du die Möglichkeiten in deinem Aufnahmegerät nutzen, oder von
|
||||||
|
Hand vor dem Hochladen der Originaldateien Anpassungen vornehmen.
|
||||||
|
|
||||||
|
Deine Daten werden mit Aktivieren dieser Funktion veröffentlicht. Wir
|
||||||
|
weisen dich darauf hin, dass die Betreiber:innen dieser Plattform dann
|
||||||
|
keine Kontrolle über mögliche Kopien dieser Daten haben, die andere davon
|
||||||
|
anfertigen. Selbst wenn du später deine Daten auf dieser Plattform
|
||||||
|
löschst oder anonymisierst können Kopien des Originals bei Dritten
|
||||||
|
vorhanden bleiben.
|
||||||
|
|
||||||
|
**Nutze diese Funktion mit Bedacht und auf dein eigenes Risiko.**
|
||||||
|
|
|
@ -13,7 +13,9 @@ general:
|
||||||
edit: Edit
|
edit: Edit
|
||||||
save: Save
|
save: Save
|
||||||
delete: Delete
|
delete: Delete
|
||||||
|
ok: OK
|
||||||
|
cancel: Cancel
|
||||||
|
confirm: Are you sure?
|
||||||
copied: Copied.
|
copied: Copied.
|
||||||
copyError: Failed to copy.
|
copyError: Failed to copy.
|
||||||
|
|
||||||
|
@ -275,3 +277,45 @@ TrackPage:
|
||||||
title: Comments
|
title: Comments
|
||||||
post: Post comment
|
post: Post comment
|
||||||
empty: Nobody commented... yet
|
empty: Nobody commented... yet
|
||||||
|
|
||||||
|
TrackEditor:
|
||||||
|
title: Edit {{trackTitle}}
|
||||||
|
replaceTrackData: Replace track data
|
||||||
|
uploadComplete: Upload complete. <1>Show track</1>
|
||||||
|
|
||||||
|
dangerZone:
|
||||||
|
title: Danger zone
|
||||||
|
description: |
|
||||||
|
You can remove this track from your account and the portal if
|
||||||
|
you like. However, if at any point you have published this
|
||||||
|
track, we cannot guarantee that there are no versions of it in
|
||||||
|
the public data repository, or any copy thereof.
|
||||||
|
confirmDelete: Are you sure you want to delete this track?
|
||||||
|
|
||||||
|
description:
|
||||||
|
label: Description
|
||||||
|
visibility:
|
||||||
|
label: Track visibility
|
||||||
|
checkboxLabel: Make track public (in track list and details page, events always visible anonymously)
|
||||||
|
description: |
|
||||||
|
The overtaking events from all tracks are always public in an anonymized
|
||||||
|
form. This option is about publishing the *full* track (where you were
|
||||||
|
cycling) for everyone to see together with your username.
|
||||||
|
|
||||||
|
Checking this box allows all users to see your full track. For your own
|
||||||
|
privacy and security, make sure to only publish tracks in this way that
|
||||||
|
do not let others deduce where you live, work, or frequently stay. Your
|
||||||
|
recording device might have useful privacy settings to not record
|
||||||
|
geolocation data near those places.
|
||||||
|
|
||||||
|
In the future, this site will allow you to redact privacy sensitive data
|
||||||
|
in tracks, both manually and automatically. Until then, you will have to
|
||||||
|
rely on the features of your recording device, or manually redact your
|
||||||
|
files before upload.
|
||||||
|
|
||||||
|
After checking this box, your data essentially becomes public. You
|
||||||
|
understand that we cannot control who potentially downloads this data and
|
||||||
|
and keeps a copy, even if you delete it from your account or anonymize it
|
||||||
|
later.
|
||||||
|
|
||||||
|
**Use at your own risk.**
|
||||||
|
|
Loading…
Reference in a new issue