Multiupload
This commit is contained in:
parent
c6ce5d6415
commit
87f5ecfc56
8 changed files with 521 additions and 45 deletions
93
src/App.js
93
src/App.js
|
@ -1,13 +1,13 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {connect} from 'react-redux'
|
import {connect} from 'react-redux'
|
||||||
import {Button} from 'semantic-ui-react'
|
import {Icon, Button} from 'semantic-ui-react'
|
||||||
import {BrowserRouter as Router, Switch, Route, Link} from 'react-router-dom'
|
import {BrowserRouter as Router, Switch, Route, Link} from 'react-router-dom'
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
|
|
||||||
import styles from './App.module.scss'
|
import styles from './App.module.scss'
|
||||||
import api from './api'
|
import api from './api'
|
||||||
|
|
||||||
import {LoginPage, LogoutPage, NotFoundPage, TracksPage, TrackPage, HomePage} from './pages'
|
import {LoginPage, LogoutPage, NotFoundPage, TracksPage, TrackPage, HomePage, UploadPage} from './pages'
|
||||||
|
|
||||||
const App = connect((state) => ({login: state.login}))(function App({login}) {
|
const App = connect((state) => ({login: state.login}))(function App({login}) {
|
||||||
// update the API header on each render, the App is rerendered when the login changes
|
// update the API header on each render, the App is rerendered when the login changes
|
||||||
|
@ -19,44 +19,56 @@ const App = connect((state) => ({login: state.login}))(function App({login}) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Router>
|
<Router>
|
||||||
<div>
|
<div className={styles.App}>
|
||||||
<header className={styles.header}>
|
<header className={styles.headline}>
|
||||||
<div className={styles.pageTitle}>OpenBikeSensor</div>
|
<div className={styles.header}>
|
||||||
<nav className={styles.menu}>
|
<div className={styles.pageTitle}>OpenBikeSensor</div>
|
||||||
<ul>
|
<nav className={styles.menu}>
|
||||||
<li>
|
<ul>
|
||||||
<Link to="/">Home</Link>
|
{login && (
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<Link to="/feed">Feed</Link>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="https://openbikesensor.org/" target="_blank">
|
|
||||||
About
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
{login ? (
|
|
||||||
<>
|
|
||||||
<li>
|
<li>
|
||||||
<Link to="/settings">Settings</Link>
|
<Link to="/upload">
|
||||||
|
<Button compact color="green">
|
||||||
|
<Icon name="cloud upload" />
|
||||||
|
Upload
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
)}
|
||||||
<Button as={Link} to="/logout">
|
<li>
|
||||||
Logout
|
<Link to="/">Home</Link>
|
||||||
</Button>
|
</li>
|
||||||
</li>
|
<li>
|
||||||
</>
|
<Link to="/feed">Feed</Link>
|
||||||
) : (
|
</li>
|
||||||
<>
|
<li>
|
||||||
<li>
|
<a href="https://openbikesensor.org/" target="_blank">
|
||||||
<Button as={Link} to="/login">
|
About
|
||||||
Login
|
</a>
|
||||||
</Button>
|
</li>
|
||||||
</li>
|
{login ? (
|
||||||
</>
|
<>
|
||||||
)}
|
<li>
|
||||||
</ul>
|
<Link to="/settings">Settings</Link>
|
||||||
</nav>
|
</li>
|
||||||
|
<li>
|
||||||
|
<Button as={Link} to="/logout" compact>
|
||||||
|
Logout
|
||||||
|
</Button>
|
||||||
|
</li>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<li>
|
||||||
|
<Button as={Link} to="/login">
|
||||||
|
Login
|
||||||
|
</Button>
|
||||||
|
</li>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<Switch>
|
<Switch>
|
||||||
|
@ -78,6 +90,11 @@ const App = connect((state) => ({login: state.login}))(function App({login}) {
|
||||||
<Route path="/logout" exact>
|
<Route path="/logout" exact>
|
||||||
<LogoutPage />
|
<LogoutPage />
|
||||||
</Route>
|
</Route>
|
||||||
|
{login && (
|
||||||
|
<Route path="/upload" exact>
|
||||||
|
<UploadPage />
|
||||||
|
</Route>
|
||||||
|
)}
|
||||||
<Route>
|
<Route>
|
||||||
<NotFoundPage />
|
<NotFoundPage />
|
||||||
</Route>
|
</Route>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
@import 'styles.scss';
|
@import 'styles.scss';
|
||||||
|
|
||||||
.App {
|
.App {
|
||||||
text-align: center;
|
padding-top: 60px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
|
@ -11,6 +11,17 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.headline {
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
position: fixed;
|
||||||
|
// border-bottom: 1px solid #E0E0E4;
|
||||||
|
background: white;
|
||||||
|
z-index: 100;
|
||||||
|
box-shadow: 0 0 10px -6px black;
|
||||||
|
}
|
||||||
|
|
||||||
.pageTitle {
|
.pageTitle {
|
||||||
font-family: 'Roboto Slab';
|
font-family: 'Roboto Slab';
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
|
14
src/api.js
14
src/api.js
|
@ -22,15 +22,19 @@ class API {
|
||||||
}
|
}
|
||||||
|
|
||||||
async post(url, {body: body_, ...options}) {
|
async post(url, {body: body_, ...options}) {
|
||||||
const body = typeof body_ !== 'string' ? JSON.stringify(body_) : body_
|
let body = body_
|
||||||
|
let headers = {...(options.headers || {})}
|
||||||
|
|
||||||
|
if (!(typeof body === 'string' || body instanceof FormData)) {
|
||||||
|
body = JSON.stringify(body)
|
||||||
|
headers['Content-Type'] = 'application/json'
|
||||||
|
}
|
||||||
|
|
||||||
return await this.fetch(url, {
|
return await this.fetch(url, {
|
||||||
...options,
|
...options,
|
||||||
body,
|
body,
|
||||||
method: 'post',
|
method: 'post',
|
||||||
headers: {
|
headers
|
||||||
...(options.headers || {}),
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
206
src/components/FileDrop.tsx
Normal file
206
src/components/FileDrop.tsx
Normal file
|
@ -0,0 +1,206 @@
|
||||||
|
// Source: https://github.com/sarink/react-file-drop/blob/master/file-drop/src/FileDrop.tsx
|
||||||
|
// Original License: MIT
|
||||||
|
// Adjusted to render prop instead of rendering div directly.
|
||||||
|
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import React, {DragEvent as ReactDragEvent, DragEventHandler as ReactDragEventHandler, ReactEventHandler} from 'react'
|
||||||
|
|
||||||
|
export type DropEffects = 'copy' | 'move' | 'link' | 'none'
|
||||||
|
|
||||||
|
export interface FileDropProps {
|
||||||
|
frame?: Exclude<HTMLElementTagNameMap[keyof HTMLElementTagNameMap], HTMLElement> | HTMLDocument
|
||||||
|
onFrameDragEnter?: (event: DragEvent) => void
|
||||||
|
onFrameDragLeave?: (event: DragEvent) => void
|
||||||
|
onFrameDrop?: (event: DragEvent) => void
|
||||||
|
onDragOver?: ReactDragEventHandler<HTMLDivElement>
|
||||||
|
onDragLeave?: ReactDragEventHandler<HTMLDivElement>
|
||||||
|
onDrop?: (files: FileList | null, event: ReactDragEvent<HTMLDivElement>) => any
|
||||||
|
onTargetClick?: ReactEventHandler<HTMLDivElement>
|
||||||
|
dropEffect?: DropEffects
|
||||||
|
children: (props: ChildrenProps) => React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FileDropState {
|
||||||
|
draggingOverFrame: boolean
|
||||||
|
draggingOverTarget: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChildrenProps extends FileDropState {
|
||||||
|
onDragOver?: ReactDragEventHandler<HTMLDivElement>
|
||||||
|
onDragLeave: ReactDragEventHandler<HTMLDivElement>
|
||||||
|
onDrop: ReactDragEventHandler<HTMLDivElement>
|
||||||
|
onClick: ReactEventHandler<HTMLDivElement>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class FileDrop extends React.PureComponent<FileDropProps, FileDropState> {
|
||||||
|
static isIE = () =>
|
||||||
|
typeof window !== 'undefined' &&
|
||||||
|
(window.navigator.userAgent.indexOf('MSIE') !== -1 || window.navigator.appVersion.indexOf('Trident/') > 0)
|
||||||
|
|
||||||
|
static eventHasFiles = (event: DragEvent | ReactDragEvent<HTMLElement>) => {
|
||||||
|
// In most browsers this is an array, but in IE11 it's an Object :(
|
||||||
|
let hasFiles = false
|
||||||
|
if (event.dataTransfer) {
|
||||||
|
const types = event.dataTransfer.types
|
||||||
|
for (const keyOrIndex in types) {
|
||||||
|
if (types[keyOrIndex] === 'Files') {
|
||||||
|
hasFiles = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return hasFiles
|
||||||
|
}
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
onDragOver: PropTypes.func,
|
||||||
|
onDragLeave: PropTypes.func,
|
||||||
|
onDrop: PropTypes.func,
|
||||||
|
onTargetClick: PropTypes.func,
|
||||||
|
dropEffect: PropTypes.oneOf(['copy', 'move', 'link', 'none']),
|
||||||
|
frame: (props: FileDropProps, propName: keyof FileDropProps, componentName: string) => {
|
||||||
|
const prop = props[propName]
|
||||||
|
if (prop == null) {
|
||||||
|
return new Error('Warning: Required prop `' + propName + '` was not specified in `' + componentName + '`')
|
||||||
|
}
|
||||||
|
if (prop !== document && !(prop instanceof HTMLElement)) {
|
||||||
|
return new Error('Warning: Prop `' + propName + '` must be one of the following: document, HTMLElement!')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onFrameDragEnter: PropTypes.func,
|
||||||
|
onFrameDragLeave: PropTypes.func,
|
||||||
|
onFrameDrop: PropTypes.func,
|
||||||
|
}
|
||||||
|
|
||||||
|
static defaultProps = {
|
||||||
|
dropEffect: 'copy' as DropEffects,
|
||||||
|
frame: typeof window === 'undefined' ? undefined : window.document,
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(props: FileDropProps) {
|
||||||
|
super(props)
|
||||||
|
this.frameDragCounter = 0
|
||||||
|
this.state = {draggingOverFrame: false, draggingOverTarget: false}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.startFrameListeners(this.props.frame)
|
||||||
|
this.resetDragging()
|
||||||
|
window.addEventListener('dragover', this.handleWindowDragOverOrDrop)
|
||||||
|
window.addEventListener('drop', this.handleWindowDragOverOrDrop)
|
||||||
|
}
|
||||||
|
|
||||||
|
UNSAFE_componentWillReceiveProps(nextProps: FileDropProps) {
|
||||||
|
if (nextProps.frame !== this.props.frame) {
|
||||||
|
this.resetDragging()
|
||||||
|
this.stopFrameListeners(this.props.frame)
|
||||||
|
this.startFrameListeners(nextProps.frame)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
this.stopFrameListeners(this.props.frame)
|
||||||
|
window.removeEventListener('dragover', this.handleWindowDragOverOrDrop)
|
||||||
|
window.removeEventListener('drop', this.handleWindowDragOverOrDrop)
|
||||||
|
}
|
||||||
|
|
||||||
|
frameDragCounter: number
|
||||||
|
|
||||||
|
resetDragging = () => {
|
||||||
|
this.frameDragCounter = 0
|
||||||
|
this.setState({draggingOverFrame: false, draggingOverTarget: false})
|
||||||
|
}
|
||||||
|
|
||||||
|
handleWindowDragOverOrDrop = (event: DragEvent) => {
|
||||||
|
// This prevents the browser from trying to load whatever file the user dropped on the window
|
||||||
|
event.preventDefault()
|
||||||
|
}
|
||||||
|
|
||||||
|
handleFrameDrag = (event: DragEvent) => {
|
||||||
|
// Only allow dragging of files
|
||||||
|
if (!FileDrop.eventHasFiles(event)) return
|
||||||
|
|
||||||
|
// We are listening for events on the 'frame', so every time the user drags over any element in the frame's tree,
|
||||||
|
// the event bubbles up to the frame. By keeping count of how many "dragenters" we get, we can tell if they are still
|
||||||
|
// "draggingOverFrame" (b/c you get one "dragenter" initially, and one "dragenter"/one "dragleave" for every bubble)
|
||||||
|
// This is far better than a "dragover" handler, which would be calling `setState` continuously.
|
||||||
|
this.frameDragCounter += event.type === 'dragenter' ? 1 : -1
|
||||||
|
|
||||||
|
if (this.frameDragCounter === 1) {
|
||||||
|
this.setState({draggingOverFrame: true})
|
||||||
|
if (this.props.onFrameDragEnter) this.props.onFrameDragEnter(event)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.frameDragCounter === 0) {
|
||||||
|
this.setState({draggingOverFrame: false})
|
||||||
|
if (this.props.onFrameDragLeave) this.props.onFrameDragLeave(event)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleFrameDrop = (event: DragEvent) => {
|
||||||
|
if (!this.state.draggingOverTarget) {
|
||||||
|
this.resetDragging()
|
||||||
|
if (this.props.onFrameDrop) this.props.onFrameDrop(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDragOver: ReactDragEventHandler<HTMLDivElement> = (event) => {
|
||||||
|
if (FileDrop.eventHasFiles(event)) {
|
||||||
|
this.setState({draggingOverTarget: true})
|
||||||
|
if (!FileDrop.isIE() && this.props.dropEffect) event.dataTransfer.dropEffect = this.props.dropEffect
|
||||||
|
if (this.props.onDragOver) this.props.onDragOver(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDragLeave: ReactDragEventHandler<HTMLDivElement> = (event) => {
|
||||||
|
this.setState({draggingOverTarget: false})
|
||||||
|
if (this.props.onDragLeave) this.props.onDragLeave(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDrop: ReactDragEventHandler<HTMLDivElement> = (event) => {
|
||||||
|
if (this.props.onDrop && FileDrop.eventHasFiles(event)) {
|
||||||
|
const files = event.dataTransfer ? event.dataTransfer.files : null
|
||||||
|
this.props.onDrop(files, event)
|
||||||
|
}
|
||||||
|
this.resetDragging()
|
||||||
|
}
|
||||||
|
|
||||||
|
handleTargetClick: ReactEventHandler<HTMLDivElement> = (event) => {
|
||||||
|
if (this.props.onTargetClick) {
|
||||||
|
this.props.onTargetClick(event)
|
||||||
|
}
|
||||||
|
this.resetDragging()
|
||||||
|
}
|
||||||
|
|
||||||
|
stopFrameListeners = (frame: FileDropProps['frame']) => {
|
||||||
|
if (frame) {
|
||||||
|
frame.removeEventListener('dragenter', this.handleFrameDrag)
|
||||||
|
frame.removeEventListener('dragleave', this.handleFrameDrag)
|
||||||
|
frame.removeEventListener('drop', this.handleFrameDrop)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
startFrameListeners = (frame: FileDropProps['frame']) => {
|
||||||
|
if (frame) {
|
||||||
|
frame.addEventListener('dragenter', this.handleFrameDrag)
|
||||||
|
frame.addEventListener('dragleave', this.handleFrameDrag)
|
||||||
|
frame.addEventListener('drop', this.handleFrameDrop)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {children} = this.props
|
||||||
|
const {draggingOverTarget, draggingOverFrame} = this.state
|
||||||
|
|
||||||
|
return children({
|
||||||
|
draggingOverFrame,
|
||||||
|
draggingOverTarget,
|
||||||
|
onDragOver: this.handleDragOver,
|
||||||
|
onDragLeave: this.handleDragLeave,
|
||||||
|
onDrop: this.handleDrop,
|
||||||
|
onClick: this.handleTargetClick,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,3 +1,4 @@
|
||||||
|
export {default as FileDrop} from './FileDrop'
|
||||||
export {default as FormattedDate} from './FormattedDate'
|
export {default as FormattedDate} from './FormattedDate'
|
||||||
export {default as LoginForm} from './LoginForm'
|
export {default as LoginForm} from './LoginForm'
|
||||||
export {default as Map} from './Map'
|
export {default as Map} from './Map'
|
||||||
|
|
|
@ -20,7 +20,7 @@ function formatDuration(seconds) {
|
||||||
|
|
||||||
function WelcomeMap() {
|
function WelcomeMap() {
|
||||||
return (
|
return (
|
||||||
<Map style={{height: '24rem', backgroundColor: '#FEFEF4'}}>
|
<Map style={{height: '24rem'}}>
|
||||||
<Map.TileLayer />
|
<Map.TileLayer />
|
||||||
</Map>
|
</Map>
|
||||||
)
|
)
|
||||||
|
|
236
src/pages/UploadPage.tsx
Normal file
236
src/pages/UploadPage.tsx
Normal file
|
@ -0,0 +1,236 @@
|
||||||
|
import _ from 'lodash'
|
||||||
|
import React from 'react'
|
||||||
|
import {List, Loader, Table, Icon, Segment, Header, Button} from 'semantic-ui-react'
|
||||||
|
import {Link} from 'react-router-dom'
|
||||||
|
|
||||||
|
import {FileDrop, Page} from 'components'
|
||||||
|
import type {Track} from 'types'
|
||||||
|
import api from 'api'
|
||||||
|
|
||||||
|
function isSameFile(a: File, b: File) {
|
||||||
|
return a.name === b.name && a.size === b.size
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatFileSize(bytes: number) {
|
||||||
|
if (bytes < 1024) {
|
||||||
|
return `${bytes} bytes`
|
||||||
|
}
|
||||||
|
|
||||||
|
bytes /= 1024
|
||||||
|
|
||||||
|
if (bytes < 1024) {
|
||||||
|
return `${bytes.toFixed(1)} KiB`
|
||||||
|
}
|
||||||
|
|
||||||
|
bytes /= 1024
|
||||||
|
|
||||||
|
if (bytes < 1024) {
|
||||||
|
return `${bytes.toFixed(1)} MiB`
|
||||||
|
}
|
||||||
|
|
||||||
|
bytes /= 1024
|
||||||
|
return `${bytes.toFixed(1)} GiB`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FileUploadResult =
|
||||||
|
| {
|
||||||
|
track: Track
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
errors: Record<string, string>
|
||||||
|
}
|
||||||
|
|
||||||
|
function FileUploadStatus({
|
||||||
|
id,
|
||||||
|
file,
|
||||||
|
onComplete,
|
||||||
|
}: {
|
||||||
|
id: string
|
||||||
|
file: File
|
||||||
|
onComplete: (result: FileUploadResult) => void
|
||||||
|
}) {
|
||||||
|
const [progress, setProgress] = React.useState(0)
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const formData = new FormData()
|
||||||
|
formData.append('body', file)
|
||||||
|
|
||||||
|
const xhr = new XMLHttpRequest()
|
||||||
|
|
||||||
|
const onProgress = (e) => {
|
||||||
|
console.log('progress', e)
|
||||||
|
const progress = (e.loaded || 0) / (e.total || 1)
|
||||||
|
setProgress(progress)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onLoad = (e) => {
|
||||||
|
console.log('loaded', e)
|
||||||
|
onComplete(id, xhr.response)
|
||||||
|
}
|
||||||
|
|
||||||
|
xhr.responseType = 'json'
|
||||||
|
xhr.onload = onLoad
|
||||||
|
xhr.upload.onprogress = onProgress
|
||||||
|
xhr.open('POST', '/api/tracks')
|
||||||
|
xhr.setRequestHeader('Authorization', api.authorization)
|
||||||
|
xhr.send(formData)
|
||||||
|
|
||||||
|
return () => xhr.abort()
|
||||||
|
}, [file])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span>
|
||||||
|
<Loader inline size="mini" active />
|
||||||
|
{' '}
|
||||||
|
{progress < 1 ? (progress * 100).toFixed(0) + ' %' : 'Processing...'}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type FileEntry = {
|
||||||
|
id: string
|
||||||
|
file?: File | null
|
||||||
|
size: number
|
||||||
|
name: string
|
||||||
|
result?: FileUploadResult
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function UploadPage() {
|
||||||
|
const labelRef = React.useRef()
|
||||||
|
const [labelRefState, setLabelRefState] = React.useState()
|
||||||
|
|
||||||
|
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]
|
||||||
|
)
|
||||||
|
|
||||||
|
React.useLayoutEffect(() => {
|
||||||
|
setLabelRefState(labelRef.current)
|
||||||
|
}, [labelRef.current])
|
||||||
|
|
||||||
|
function onSelectFiles(fileList) {
|
||||||
|
console.log('UPLOAD', fileList)
|
||||||
|
|
||||||
|
const newFiles = Array.from(fileList).map((file) => ({
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
|
||||||
|
function onChangeField(e) {
|
||||||
|
if (e.target.files && e.target.files.length) {
|
||||||
|
onSelectFiles(e.target.files)
|
||||||
|
}
|
||||||
|
e.target.value = '' // reset the form field for uploading again
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onDeleteTrack(slug: string) {
|
||||||
|
await api.delete(`/tracks/${slug}`)
|
||||||
|
setFiles((files) => files.filter((t) => t.result?.track?.slug !== slug))
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Page>
|
||||||
|
{files.length ? (
|
||||||
|
<Table>
|
||||||
|
<Table.Header>
|
||||||
|
<Table.Row>
|
||||||
|
<Table.HeaderCell>Filename</Table.HeaderCell>
|
||||||
|
<Table.HeaderCell>Size</Table.HeaderCell>
|
||||||
|
<Table.HeaderCell>Status</Table.HeaderCell>
|
||||||
|
<Table.HeaderCell></Table.HeaderCell>
|
||||||
|
<Table.HeaderCell></Table.HeaderCell>
|
||||||
|
</Table.Row>
|
||||||
|
</Table.Header>
|
||||||
|
|
||||||
|
<Table.Body>
|
||||||
|
{files.map(({id, name, size, file, result}) => (
|
||||||
|
<Table.Row key={id}>
|
||||||
|
<Table.Cell>
|
||||||
|
<Icon name="file" />
|
||||||
|
{name}
|
||||||
|
</Table.Cell>
|
||||||
|
<Table.Cell>{formatFileSize(size)}</Table.Cell>
|
||||||
|
<Table.Cell>
|
||||||
|
{result ? (
|
||||||
|
<>
|
||||||
|
<Icon name="check" /> Uploaded
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<FileUploadStatus {...{id, file}} onComplete={onCompleteFileUpload} />
|
||||||
|
)}
|
||||||
|
</Table.Cell>
|
||||||
|
<Table.Cell>
|
||||||
|
{/* <pre>{JSON.stringify(result || null, null, 2)}</pre> */}
|
||||||
|
{result?.errors ? (
|
||||||
|
<List>
|
||||||
|
{_.sortBy(Object.entries(result.errors))
|
||||||
|
.filter(([field, message]) => typeof message === 'string')
|
||||||
|
.map(([field, message]) => (
|
||||||
|
<List.Item key={field}>
|
||||||
|
<List.Icon name="warning sign" color="red" />
|
||||||
|
<List.Content>{message}</List.Content>
|
||||||
|
</List.Item>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
) : null}
|
||||||
|
</Table.Cell>
|
||||||
|
<Table.Cell>
|
||||||
|
{result?.track ? (
|
||||||
|
<>
|
||||||
|
<Link to={`/tracks/${result.track.slug}`}>
|
||||||
|
<Button size="small" icon="arrow right" />
|
||||||
|
</Link>
|
||||||
|
<Button size="small" icon="trash" onClick={() => onDeleteTrack(result.track.slug)} />
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
</Table.Cell>
|
||||||
|
</Table.Row>
|
||||||
|
))}
|
||||||
|
</Table.Body>
|
||||||
|
</Table>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
id="upload-field"
|
||||||
|
style={{width: 0, height: 0, position: 'fixed', left: -1000, top: -1000, opacity: 0.001}}
|
||||||
|
multiple
|
||||||
|
accept=".csv"
|
||||||
|
onChange={onChangeField}
|
||||||
|
/>
|
||||||
|
<label htmlFor="upload-field" ref={labelRef}>
|
||||||
|
{labelRefState && (
|
||||||
|
<FileDrop onDrop={onSelectFiles} frame={labelRefState}>
|
||||||
|
{({draggingOverFrame, draggingOverTarget, onDragOver, onDragLeave, onDrop, onClick}) => (
|
||||||
|
<Segment
|
||||||
|
placeholder
|
||||||
|
{...{onDragOver, onDragLeave, onDrop}}
|
||||||
|
style={{
|
||||||
|
background: draggingOverTarget || draggingOverFrame ? '#E0E0EE' : null,
|
||||||
|
transition: 'background 0.2s',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Header icon>
|
||||||
|
<Icon name="cloud upload" />
|
||||||
|
Drop files here or click to select them for upload
|
||||||
|
</Header>
|
||||||
|
|
||||||
|
<Button primary as="span">
|
||||||
|
Upload files
|
||||||
|
</Button>
|
||||||
|
</Segment>
|
||||||
|
)}
|
||||||
|
</FileDrop>
|
||||||
|
)}
|
||||||
|
</label>
|
||||||
|
</Page>
|
||||||
|
)
|
||||||
|
}
|
|
@ -4,3 +4,4 @@ export {default as LogoutPage} from './LogoutPage'
|
||||||
export {default as NotFoundPage} from './NotFoundPage'
|
export {default as NotFoundPage} from './NotFoundPage'
|
||||||
export {default as TrackPage} from './TrackPage'
|
export {default as TrackPage} from './TrackPage'
|
||||||
export {default as TracksPage} from './TracksPage'
|
export {default as TracksPage} from './TracksPage'
|
||||||
|
export {default as UploadPage} from './UploadPage'
|
||||||
|
|
Loading…
Add table
Reference in a new issue