Translate UploadPage
This commit is contained in:
parent
a85379418e
commit
2cff606092
|
@ -1,29 +1,31 @@
|
|||
import React from 'react'
|
||||
import {Icon, Segment, Header, Button} from 'semantic-ui-react'
|
||||
import React from "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}) {
|
||||
const labelRef = React.useRef()
|
||||
const [labelRefState, setLabelRefState] = React.useState()
|
||||
export default function FileUploadField({ onSelect: onSelect_, multiple }) {
|
||||
const { t } = useTranslation();
|
||||
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(
|
||||
() => {
|
||||
setLabelRefState(labelRef.current)
|
||||
setLabelRefState(labelRef.current);
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[labelRef.current]
|
||||
)
|
||||
);
|
||||
|
||||
function onChangeField(e) {
|
||||
e.preventDefault?.()
|
||||
e.preventDefault?.();
|
||||
|
||||
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 (
|
||||
|
@ -31,7 +33,14 @@ export default function FileUploadField({onSelect: onSelect_, multiple}) {
|
|||
<input
|
||||
type="file"
|
||||
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}
|
||||
accept=".csv"
|
||||
onChange={onChangeField}
|
||||
|
@ -39,28 +48,40 @@ export default function FileUploadField({onSelect: onSelect_, multiple}) {
|
|||
<label htmlFor="upload-field" ref={labelRef}>
|
||||
{labelRefState && (
|
||||
<FileDrop onDrop={onSelect} frame={labelRefState}>
|
||||
{({draggingOverFrame, draggingOverTarget, onDragOver, onDragLeave, onDrop, onClick}) => (
|
||||
{({
|
||||
draggingOverFrame,
|
||||
draggingOverTarget,
|
||||
onDragOver,
|
||||
onDragLeave,
|
||||
onDrop,
|
||||
onClick,
|
||||
}) => (
|
||||
<Segment
|
||||
placeholder
|
||||
{...{onDragOver, onDragLeave, onDrop}}
|
||||
{...{ onDragOver, onDragLeave, onDrop }}
|
||||
style={{
|
||||
background: draggingOverTarget || draggingOverFrame ? '#E0E0EE' : null,
|
||||
transition: 'background 0.2s',
|
||||
background:
|
||||
draggingOverTarget || draggingOverFrame ? "#E0E0EE" : null,
|
||||
transition: "background 0.2s",
|
||||
}}
|
||||
>
|
||||
<Header icon>
|
||||
<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>
|
||||
|
||||
<Button primary as="span">
|
||||
Upload file{multiple ? 's' : ''}
|
||||
{multiple
|
||||
? t("FileUploadField.uploadFiles")
|
||||
: t("FileUploadField.uploadFile")}
|
||||
</Button>
|
||||
</Segment>
|
||||
)}
|
||||
</FileDrop>
|
||||
)}
|
||||
</label>
|
||||
</>
|
||||
)
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,45 +1,46 @@
|
|||
import _ from 'lodash'
|
||||
import React from 'react'
|
||||
import {List, Loader, Table, Icon} from 'semantic-ui-react'
|
||||
import {Link} from 'react-router-dom'
|
||||
import _ from "lodash";
|
||||
import React from "react";
|
||||
import { List, Loader, Table, Icon } from "semantic-ui-react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import {FileUploadField, Page} from 'components'
|
||||
import type {Track} from 'types'
|
||||
import api from 'api'
|
||||
import configPromise from 'config'
|
||||
import { FileUploadField, Page } from "components";
|
||||
import type { Track } from "types";
|
||||
import api from "api";
|
||||
import configPromise from "config";
|
||||
|
||||
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) {
|
||||
if (bytes < 1024) {
|
||||
return `${bytes} bytes`
|
||||
return `${bytes} bytes`;
|
||||
}
|
||||
|
||||
bytes /= 1024
|
||||
bytes /= 1024;
|
||||
|
||||
if (bytes < 1024) {
|
||||
return `${bytes.toFixed(1)} KiB`
|
||||
return `${bytes.toFixed(1)} KiB`;
|
||||
}
|
||||
|
||||
bytes /= 1024
|
||||
bytes /= 1024;
|
||||
|
||||
if (bytes < 1024) {
|
||||
return `${bytes.toFixed(1)} MiB`
|
||||
return `${bytes.toFixed(1)} MiB`;
|
||||
}
|
||||
|
||||
bytes /= 1024
|
||||
return `${bytes.toFixed(1)} GiB`
|
||||
bytes /= 1024;
|
||||
return `${bytes.toFixed(1)} GiB`;
|
||||
}
|
||||
|
||||
type FileUploadResult =
|
||||
| {
|
||||
track: Track
|
||||
track: Track;
|
||||
}
|
||||
| {
|
||||
errors: Record<string, string>
|
||||
}
|
||||
errors: Record<string, string>;
|
||||
};
|
||||
|
||||
export function FileUploadStatus({
|
||||
id,
|
||||
|
@ -47,108 +48,127 @@ export function FileUploadStatus({
|
|||
onComplete,
|
||||
slug,
|
||||
}: {
|
||||
id: string
|
||||
file: File
|
||||
onComplete: (id: string, result: FileUploadResult) => void
|
||||
slug?: string
|
||||
id: string;
|
||||
file: File;
|
||||
onComplete: (id: string, result: FileUploadResult) => void;
|
||||
slug?: string;
|
||||
}) {
|
||||
const [progress, setProgress] = React.useState(0)
|
||||
const [progress, setProgress] = React.useState(0);
|
||||
|
||||
React.useEffect(
|
||||
() => {
|
||||
let xhr
|
||||
let xhr;
|
||||
|
||||
async function _work() {
|
||||
const formData = new FormData()
|
||||
formData.append('body', file)
|
||||
const formData = new FormData();
|
||||
formData.append("body", file);
|
||||
|
||||
xhr = new XMLHttpRequest()
|
||||
xhr.withCredentials = true
|
||||
xhr = new XMLHttpRequest();
|
||||
xhr.withCredentials = true;
|
||||
|
||||
const onProgress = (e) => {
|
||||
const progress = (e.loaded || 0) / (e.total || 1)
|
||||
setProgress(progress)
|
||||
}
|
||||
const progress = (e.loaded || 0) / (e.total || 1);
|
||||
setProgress(progress);
|
||||
};
|
||||
|
||||
const onLoad = (e) => {
|
||||
onComplete(id, xhr.response)
|
||||
}
|
||||
onComplete(id, xhr.response);
|
||||
};
|
||||
|
||||
xhr.responseType = 'json'
|
||||
xhr.onload = onLoad
|
||||
xhr.upload.onprogress = onProgress
|
||||
xhr.responseType = "json";
|
||||
xhr.onload = onLoad;
|
||||
xhr.upload.onprogress = onProgress;
|
||||
|
||||
const config = await configPromise
|
||||
const config = await configPromise;
|
||||
if (slug) {
|
||||
xhr.open('PUT', `${config.apiUrl}/tracks/${slug}`)
|
||||
xhr.open("PUT", `${config.apiUrl}/tracks/${slug}`);
|
||||
} else {
|
||||
xhr.open('POST', `${config.apiUrl}/tracks`)
|
||||
xhr.open("POST", `${config.apiUrl}/tracks`);
|
||||
}
|
||||
|
||||
// const accessToken = await api.getValidAccessToken()
|
||||
|
||||
// xhr.setRequestHeader('Authorization', accessToken)
|
||||
xhr.send(formData)
|
||||
xhr.send(formData);
|
||||
}
|
||||
|
||||
_work()
|
||||
return () => xhr.abort()
|
||||
_work();
|
||||
return () => xhr.abort();
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[file]
|
||||
)
|
||||
);
|
||||
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<span>
|
||||
<Loader inline size="mini" active />{' '}
|
||||
{progress < 1 ? `Uploading ${(progress * 100).toFixed(0)}%` : 'Processing...'}
|
||||
<Loader inline size="mini" active />{" "}
|
||||
{progress < 1
|
||||
? t("UploadPage.uploadProgress", {
|
||||
progress: (progress * 100).toFixed(0),
|
||||
})
|
||||
: t("UploadPage.processing")}
|
||||
</span>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
type FileEntry = {
|
||||
id: string
|
||||
file?: File | null
|
||||
size: number
|
||||
name: string
|
||||
result?: FileUploadResult
|
||||
}
|
||||
id: string;
|
||||
file?: File | null;
|
||||
size: number;
|
||||
name: string;
|
||||
result?: FileUploadResult;
|
||||
};
|
||||
|
||||
export default function UploadPage() {
|
||||
const [files, setFiles] = React.useState<FileEntry[]>([])
|
||||
const [files, setFiles] = React.useState<FileEntry[]>([]);
|
||||
|
||||
const onCompleteFileUpload = React.useCallback(
|
||||
(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]
|
||||
)
|
||||
);
|
||||
|
||||
function onSelectFiles(fileList) {
|
||||
const newFiles = Array.from(fileList).map((file) => ({
|
||||
id: 'file-' + String(Math.floor(Math.random() * 1000000)),
|
||||
id: "file-" + String(Math.floor(Math.random() * 1000000)),
|
||||
file,
|
||||
name: file.name,
|
||||
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 (
|
||||
<Page title="Upload">
|
||||
{files.length ? (
|
||||
<Table>
|
||||
<Table.Header>
|
||||
<Table.Row>
|
||||
<Table.HeaderCell>Filename</Table.HeaderCell>
|
||||
<Table.HeaderCell>Size</Table.HeaderCell>
|
||||
<Table.HeaderCell>Status / Title</Table.HeaderCell>
|
||||
<Table.HeaderCell>
|
||||
{t("UploadPage.table.filename")}
|
||||
</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.Row>
|
||||
</Table.Header>
|
||||
|
||||
<Table.Body>
|
||||
{files.map(({id, name, size, file, result}) => (
|
||||
{files.map(({ id, name, size, file, result }) => (
|
||||
<Table.Row key={id}>
|
||||
<Table.Cell>
|
||||
<Icon name="file" />
|
||||
|
@ -159,7 +179,9 @@ export default function UploadPage() {
|
|||
{result?.errors ? (
|
||||
<List>
|
||||
{_.sortBy(Object.entries(result.errors))
|
||||
.filter(([field, message]) => typeof message === 'string')
|
||||
.filter(
|
||||
([field, message]) => typeof message === "string"
|
||||
)
|
||||
.map(([field, message]) => (
|
||||
<List.Item key={field}>
|
||||
<List.Icon name="warning sign" color="red" />
|
||||
|
@ -169,15 +191,29 @@ export default function UploadPage() {
|
|||
</List>
|
||||
) : 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>{result?.track ? <Link to={`/tracks/${result.track.slug}`}>Show</Link> : null}</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.Row>
|
||||
))}
|
||||
|
@ -187,5 +223,5 @@ export default function UploadPage() {
|
|||
|
||||
<FileUploadField onSelect={onSelectFiles} multiple />
|
||||
</Page>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
@ -3,6 +3,8 @@ general:
|
|||
unnamedTrack: Unbenannte Fahrt
|
||||
public: Öffentlich
|
||||
private: Privat
|
||||
show: Anzeigen
|
||||
edit: Bearbeiten
|
||||
|
||||
App:
|
||||
footer:
|
||||
|
@ -87,3 +89,19 @@ ExportPage:
|
|||
shapefile: Shapefile (ZIP)
|
||||
boundingBox:
|
||||
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
|
||||
|
|
|
@ -7,6 +7,8 @@ general:
|
|||
unnamedTrack: Unnamed track
|
||||
public: Public
|
||||
private: Private
|
||||
show: Show
|
||||
edit: Edit
|
||||
|
||||
App:
|
||||
footer:
|
||||
|
@ -92,3 +94,19 @@ ExportPage:
|
|||
shapefile: Shapefile (ZIP)
|
||||
boundingBox:
|
||||
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
|
||||
|
|
Loading…
Reference in a new issue