Translate TrackEditor

This commit is contained in:
Paul Bienkowski 2022-07-24 22:12:38 +02:00
parent 6f7c8d54f2
commit f158414f24
5 changed files with 268 additions and 159 deletions

View file

@ -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;

View file

@ -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>
); );

View file

@ -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'

View file

@ -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.**

View file

@ -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.**