Translate UploadPage

This commit is contained in:
Paul Bienkowski 2022-07-24 18:07:47 +02:00
parent a85379418e
commit 2cff606092
4 changed files with 183 additions and 90 deletions

View file

@ -1,29 +1,31 @@
import React from 'react' import React from "react";
import {Icon, Segment, Header, Button} from 'semantic-ui-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}) { export default function FileUploadField({ onSelect: onSelect_, multiple }) {
const labelRef = React.useRef() const { t } = useTranslation();
const [labelRefState, setLabelRefState] = React.useState() 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( React.useLayoutEffect(
() => { () => {
setLabelRefState(labelRef.current) setLabelRefState(labelRef.current);
}, },
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
[labelRef.current] [labelRef.current]
) );
function onChangeField(e) { function onChangeField(e) {
e.preventDefault?.() e.preventDefault?.();
if (e.target.files && e.target.files.length) { 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 ( return (
@ -31,7 +33,14 @@ export default function FileUploadField({onSelect: onSelect_, multiple}) {
<input <input
type="file" type="file"
id="upload-field" id="upload-field"
style={{width: 0, height: 0, position: 'fixed', left: -1000, top: -1000, opacity: 0.001}} style={{
width: 0,
height: 0,
position: "fixed",
left: -1000,
top: -1000,
opacity: 0.001,
}}
multiple={multiple} multiple={multiple}
accept=".csv" accept=".csv"
onChange={onChangeField} onChange={onChangeField}
@ -39,28 +48,40 @@ export default function FileUploadField({onSelect: onSelect_, multiple}) {
<label htmlFor="upload-field" ref={labelRef}> <label htmlFor="upload-field" ref={labelRef}>
{labelRefState && ( {labelRefState && (
<FileDrop onDrop={onSelect} frame={labelRefState}> <FileDrop onDrop={onSelect} frame={labelRefState}>
{({draggingOverFrame, draggingOverTarget, onDragOver, onDragLeave, onDrop, onClick}) => ( {({
draggingOverFrame,
draggingOverTarget,
onDragOver,
onDragLeave,
onDrop,
onClick,
}) => (
<Segment <Segment
placeholder placeholder
{...{onDragOver, onDragLeave, onDrop}} {...{ onDragOver, onDragLeave, onDrop }}
style={{ style={{
background: draggingOverTarget || draggingOverFrame ? '#E0E0EE' : null, background:
transition: 'background 0.2s', draggingOverTarget || draggingOverFrame ? "#E0E0EE" : null,
transition: "background 0.2s",
}} }}
> >
<Header icon> <Header icon>
<Icon name="cloud upload" /> <Icon name="cloud upload" />
Drop file{multiple ? 's' : ''} here or click to select {multiple ? 'them' : 'one'} for upload {multiple
? t("FileUploadField.dropOrClickMultiple")
: t("FileUploadField.dropOrClick")}
</Header> </Header>
<Button primary as="span"> <Button primary as="span">
Upload file{multiple ? 's' : ''} {multiple
? t("FileUploadField.uploadFiles")
: t("FileUploadField.uploadFile")}
</Button> </Button>
</Segment> </Segment>
)} )}
</FileDrop> </FileDrop>
)} )}
</label> </label>
</> </>
) );
} }

View file

@ -1,45 +1,46 @@
import _ from 'lodash' import _ from "lodash";
import React from 'react' import React from "react";
import {List, Loader, Table, Icon} from 'semantic-ui-react' import { List, Loader, Table, Icon } from "semantic-ui-react";
import {Link} from 'react-router-dom' import { Link } from "react-router-dom";
import { useTranslation } from "react-i18next";
import {FileUploadField, Page} from 'components' import { FileUploadField, Page } from "components";
import type {Track} from 'types' import type { Track } from "types";
import api from 'api' import api from "api";
import configPromise from 'config' import configPromise from "config";
function isSameFile(a: File, b: File) { function isSameFile(a: File, b: File) {
return a.name === b.name && a.size === b.size return a.name === b.name && a.size === b.size;
} }
function formatFileSize(bytes: number) { function formatFileSize(bytes: number) {
if (bytes < 1024) { if (bytes < 1024) {
return `${bytes} bytes` return `${bytes} bytes`;
} }
bytes /= 1024 bytes /= 1024;
if (bytes < 1024) { if (bytes < 1024) {
return `${bytes.toFixed(1)} KiB` return `${bytes.toFixed(1)} KiB`;
} }
bytes /= 1024 bytes /= 1024;
if (bytes < 1024) { if (bytes < 1024) {
return `${bytes.toFixed(1)} MiB` return `${bytes.toFixed(1)} MiB`;
} }
bytes /= 1024 bytes /= 1024;
return `${bytes.toFixed(1)} GiB` return `${bytes.toFixed(1)} GiB`;
} }
type FileUploadResult = type FileUploadResult =
| { | {
track: Track track: Track;
} }
| { | {
errors: Record<string, string> errors: Record<string, string>;
} };
export function FileUploadStatus({ export function FileUploadStatus({
id, id,
@ -47,108 +48,127 @@ export function FileUploadStatus({
onComplete, onComplete,
slug, slug,
}: { }: {
id: string id: string;
file: File file: File;
onComplete: (id: string, result: FileUploadResult) => void onComplete: (id: string, result: FileUploadResult) => void;
slug?: string slug?: string;
}) { }) {
const [progress, setProgress] = React.useState(0) const [progress, setProgress] = React.useState(0);
React.useEffect( React.useEffect(
() => { () => {
let xhr let xhr;
async function _work() { async function _work() {
const formData = new FormData() const formData = new FormData();
formData.append('body', file) formData.append("body", file);
xhr = new XMLHttpRequest() xhr = new XMLHttpRequest();
xhr.withCredentials = true xhr.withCredentials = true;
const onProgress = (e) => { const onProgress = (e) => {
const progress = (e.loaded || 0) / (e.total || 1) const progress = (e.loaded || 0) / (e.total || 1);
setProgress(progress) setProgress(progress);
} };
const onLoad = (e) => { const onLoad = (e) => {
onComplete(id, xhr.response) onComplete(id, xhr.response);
} };
xhr.responseType = 'json' xhr.responseType = "json";
xhr.onload = onLoad xhr.onload = onLoad;
xhr.upload.onprogress = onProgress xhr.upload.onprogress = onProgress;
const config = await configPromise const config = await configPromise;
if (slug) { if (slug) {
xhr.open('PUT', `${config.apiUrl}/tracks/${slug}`) xhr.open("PUT", `${config.apiUrl}/tracks/${slug}`);
} else { } else {
xhr.open('POST', `${config.apiUrl}/tracks`) xhr.open("POST", `${config.apiUrl}/tracks`);
} }
// const accessToken = await api.getValidAccessToken() // const accessToken = await api.getValidAccessToken()
// xhr.setRequestHeader('Authorization', accessToken) // xhr.setRequestHeader('Authorization', accessToken)
xhr.send(formData) xhr.send(formData);
} }
_work() _work();
return () => xhr.abort() return () => xhr.abort();
}, },
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
[file] [file]
) );
const { t } = useTranslation();
return ( return (
<span> <span>
<Loader inline size="mini" active />{' '} <Loader inline size="mini" active />{" "}
{progress < 1 ? `Uploading ${(progress * 100).toFixed(0)}%` : 'Processing...'} {progress < 1
? t("UploadPage.uploadProgress", {
progress: (progress * 100).toFixed(0),
})
: t("UploadPage.processing")}
</span> </span>
) );
} }
type FileEntry = { type FileEntry = {
id: string id: string;
file?: File | null file?: File | null;
size: number size: number;
name: string name: string;
result?: FileUploadResult result?: FileUploadResult;
} };
export default function UploadPage() { export default function UploadPage() {
const [files, setFiles] = React.useState<FileEntry[]>([]) const [files, setFiles] = React.useState<FileEntry[]>([]);
const onCompleteFileUpload = React.useCallback( const onCompleteFileUpload = React.useCallback(
(id, result) => { (id, result) => {
setFiles((files) => files.map((file) => (file.id === id ? {...file, result, file: null} : file))) setFiles((files) =>
files.map((file) =>
file.id === id ? { ...file, result, file: null } : file
)
);
}, },
[setFiles] [setFiles]
) );
function onSelectFiles(fileList) { function onSelectFiles(fileList) {
const newFiles = Array.from(fileList).map((file) => ({ const newFiles = Array.from(fileList).map((file) => ({
id: 'file-' + String(Math.floor(Math.random() * 1000000)), id: "file-" + String(Math.floor(Math.random() * 1000000)),
file, file,
name: file.name, name: file.name,
size: file.size, size: file.size,
})) }));
setFiles(files.filter((a) => !newFiles.some((b) => isSameFile(a, b))).concat(newFiles)) setFiles(
files
.filter((a) => !newFiles.some((b) => isSameFile(a, b)))
.concat(newFiles)
);
} }
const { t } = useTranslation();
return ( return (
<Page title="Upload"> <Page title="Upload">
{files.length ? ( {files.length ? (
<Table> <Table>
<Table.Header> <Table.Header>
<Table.Row> <Table.Row>
<Table.HeaderCell>Filename</Table.HeaderCell> <Table.HeaderCell>
<Table.HeaderCell>Size</Table.HeaderCell> {t("UploadPage.table.filename")}
<Table.HeaderCell>Status / Title</Table.HeaderCell> </Table.HeaderCell>
<Table.HeaderCell>{t("UploadPage.table.size")}</Table.HeaderCell>
<Table.HeaderCell>
{t("UploadPage.table.statusTitle")}
</Table.HeaderCell>
<Table.HeaderCell colSpan={2}></Table.HeaderCell> <Table.HeaderCell colSpan={2}></Table.HeaderCell>
</Table.Row> </Table.Row>
</Table.Header> </Table.Header>
<Table.Body> <Table.Body>
{files.map(({id, name, size, file, result}) => ( {files.map(({ id, name, size, file, result }) => (
<Table.Row key={id}> <Table.Row key={id}>
<Table.Cell> <Table.Cell>
<Icon name="file" /> <Icon name="file" />
@ -159,7 +179,9 @@ export default function UploadPage() {
{result?.errors ? ( {result?.errors ? (
<List> <List>
{_.sortBy(Object.entries(result.errors)) {_.sortBy(Object.entries(result.errors))
.filter(([field, message]) => typeof message === 'string') .filter(
([field, message]) => typeof message === "string"
)
.map(([field, message]) => ( .map(([field, message]) => (
<List.Item key={field}> <List.Item key={field}>
<List.Icon name="warning sign" color="red" /> <List.Icon name="warning sign" color="red" />
@ -169,15 +191,29 @@ export default function UploadPage() {
</List> </List>
) : result ? ( ) : result ? (
<> <>
<Icon name="check" /> {result.track?.title || 'Unnamed track'} <Icon name="check" />{" "}
{result.track?.title || t("general.unnamedTrack")}
</> </>
) : ( ) : (
<FileUploadStatus {...{id, file}} onComplete={onCompleteFileUpload} /> <FileUploadStatus
{...{ id, file }}
onComplete={onCompleteFileUpload}
/>
)} )}
</Table.Cell> </Table.Cell>
<Table.Cell>{result?.track ? <Link to={`/tracks/${result.track.slug}`}>Show</Link> : null}</Table.Cell>
<Table.Cell> <Table.Cell>
{result?.track ? <Link to={`/tracks/${result.track.slug}/edit`}>Edit</Link> : null} {result?.track ? (
<Link to={`/tracks/${result.track.slug}`}>
{t("general.show")}
</Link>
) : null}
</Table.Cell>
<Table.Cell>
{result?.track ? (
<Link to={`/tracks/${result.track.slug}/edit`}>
{t("general.edit")}
</Link>
) : null}
</Table.Cell> </Table.Cell>
</Table.Row> </Table.Row>
))} ))}
@ -187,5 +223,5 @@ export default function UploadPage() {
<FileUploadField onSelect={onSelectFiles} multiple /> <FileUploadField onSelect={onSelectFiles} multiple />
</Page> </Page>
) );
} }

View file

@ -3,6 +3,8 @@ general:
unnamedTrack: Unbenannte Fahrt unnamedTrack: Unbenannte Fahrt
public: Öffentlich public: Öffentlich
private: Privat private: Privat
show: Anzeigen
edit: Bearbeiten
App: App:
footer: footer:
@ -87,3 +89,19 @@ ExportPage:
shapefile: Shapefile (ZIP) shapefile: Shapefile (ZIP)
boundingBox: boundingBox:
label: Geografischer Bereich label: Geografischer Bereich
UploadPage:
uploadProgress: Lade hoch {progress}%
processing: Verarbeiten...
table:
filename: Dateiname
size: Größe
statusTitle: Status / Titel
FileUploadField:
dropOrClick: Datei hierher ziehen oder klicken, um eine zum Hochladen auszuwählen
dropOrClickMultiple: Dateien hierher ziehen oder klicken, um Dateien zum Hochladen auszuwählen
uploadFile: Datei hochladen
uploadFiles: Dateien hochladen

View file

@ -7,6 +7,8 @@ general:
unnamedTrack: Unnamed track unnamedTrack: Unnamed track
public: Public public: Public
private: Private private: Private
show: Show
edit: Edit
App: App:
footer: footer:
@ -92,3 +94,19 @@ ExportPage:
shapefile: Shapefile (ZIP) shapefile: Shapefile (ZIP)
boundingBox: boundingBox:
label: Bounding Box label: Bounding Box
UploadPage:
uploadProgress: Uploading {progress}%
processing: Processing...
table:
filename: Filename
size: Size
statusTitle: Status / Title
FileUploadField:
dropOrClick: Drop file here or click to select one for upload
dropOrClickMultiple: Drop files here or click to select them for upload
uploadFile: Upload file
uploadFiles: Upload files