Table based upload flow

This commit is contained in:
Jordan Eldredge 2020-09-15 22:36:52 -07:00
parent 4595d32137
commit 8cd744d3e3
8 changed files with 284 additions and 220 deletions

View file

@ -12,7 +12,7 @@ import * as Actions from "./redux/actionCreators";
import { ABOUT_PAGE } from "./constants";
import { useWindowSize, useScrollbarWidth, useActionCreator } from "./hooks";
import { SCREENSHOT_WIDTH, SKIN_RATIO } from "./constants";
import UploadGrid from "./UploadGrid";
import UploadGrid from "./upload/UploadGrid";
import Metadata from "./components/Metadata";
import SkinReadme from "./SkinReadme";
import { useDropzone } from "react-dropzone";

View file

@ -1,215 +0,0 @@
import React, { useMemo } from "react";
import { useSelector } from "react-redux";
import { HEADING_HEIGHT } from "./constants";
import { useActionCreator } from "./hooks";
import * as Actions from "./redux/actionCreators";
import * as Utils from "./utils";
import DropTarget from "./DropTarget";
function Section({ files, title, render, open = false }) {
if (files.length === 0) {
return null;
}
return (
<details open={open}>
<summary>
<h2 style={{ display: "inline" }}>{title}</h2>
</summary>
<ul>
{files.map((match, i) => {
return <li key={i}>{render(match)}</li>;
})}
</ul>
</details>
);
}
function useBucketed(filesArr) {
return useMemo(() => {
const missing = [];
const notSkins = [];
const foundSkins = [];
filesArr.forEach((file) => {
switch (file.status) {
case "MISSING":
case "UPLOADING":
case "UPLOAD_FAILED":
case "ARCHIVED":
missing.push(file);
break;
case "INVALID_FILE_EXTENSION":
case "INVALID_ARCHIVE":
notSkins.push(file);
break;
case "FOUND":
foundSkins.push(file);
break;
default:
}
});
return { missing, notSkins, foundSkins };
}, [filesArr]);
}
function Plural({ count, single, plural }) {
return count === 1 ? single : plural;
}
function Inner({ files }) {
const tryToUploadAllFiles = useActionCreator(Actions.tryToUploadAllFiles);
const filesArr = Object.values(files);
const { missing, notSkins, foundSkins } = useBucketed(filesArr);
if (filesArr.some((file) => file.status === "NEW")) {
return (
<h1>
Analyzing {filesArr.length.toLocaleString()}{" "}
<Plural count={filesArr.length} single="file" plural="files" />
...
</h1>
);
}
return (
<>
<h1>
You have <strong>{missing.length.toLocaleString()}</strong>{" "}
<Plural count={missing.length} single="skin" plural="skins" /> that we
are missing!
</h1>
{filesArr.some((file) => file.status === "MISSING") && (
<div>
<button onClick={tryToUploadAllFiles}>Upload All</button>
</div>
)}
<table>
<thead style={{ textAlign: "left" }}>
<tr>
<th>Status</th>
<th>Filename</th>
</tr>
</thead>
<tbody>
{missing
.map((file) => {
const fileName = <code>{file.file.name}</code>;
switch (file.status) {
case "MISSING":
return (
<>
<td />
<td>{fileName}</td>
</>
);
case "UPLOADING":
return (
<>
<td>🚀 Uploading...</td>
<td>{fileName}</td>
</>
);
case "UPLOAD_FAILED":
return (
<>
<td> Upload Failed</td>
<td>{fileName}</td>
</>
);
case "ARCHIVED":
return (
<>
<td> Added!</td>
<td>
{file.skinType === "CLASSIC" ? (
<a
href={Utils.museumUrlFromHash(file.md5)}
target="_blank"
rel="noopener noreferrer"
>
{fileName}
</a>
) : file.skinType === "MODERN" ? (
<>
{fileName} (Note: Modern skins are not yet visible
in the museum)
</>
) : // TODO: Throw?
null}
</td>
</>
);
default:
console.error(`Unexpected file status: ${file.status}`);
return null;
}
})
.map((rows, i) => (
<tr key={i}>{rows}</tr>
))}
</tbody>
</table>
<Section
files={notSkins}
title={
<>
Files that are not valid skins ({notSkins.length.toLocaleString()})
</>
}
render={(file) => <code>{file.file.name}</code>}
/>
<Section
files={foundSkins}
title={
<>Already in the museum ({foundSkins.length.toLocaleString()})</>
}
render={(file) => {
if (file.skinType === "MODERN") {
return (
<>
<code>{file.file.name}</code> (Note: Modern skins are not yet
visible in the museum)
</>
);
}
return (
<a
href={Utils.museumUrlFromHash(file.md5)}
target="_blank"
rel="noopener noreferrer"
>
<code>{file.file.name}</code>
</a>
);
}}
/>
</>
);
}
function UploadGrid({ getInputProps, isDragActive, ...props }) {
const files = useSelector((state) => state.fileUploads);
return (
<div
style={{
position: "absolute",
top: HEADING_HEIGHT,
left: 0,
bottom: 0,
right: 0,
display: "flex",
flexDirection: "column",
color: "lightgrey",
}}
>
{isDragActive || Object.keys(files).length === 0 ? (
<DropTarget getInputProps={getInputProps} />
) : (
<div style={{ padding: 15 }}>
<Inner files={files} {...props} />
</div>
)}
</div>
);
}
export default UploadGrid;

View file

@ -2,7 +2,7 @@ import React, { useState } from "react";
import DownloadLink from "./DownloadLink";
import * as Utils from "../utils";
import LinkInput from "./LinkInput";
import { API_URL } from "../constants";
// import { API_URL } from "../constants";
// import * as Actions from "../redux/actionCreators";
import * as Selectors from "../redux/selectors";
import * as Actions from "../redux/actionCreators";

View file

@ -18,7 +18,7 @@ import {
import { search } from "../algolia";
import queryParser from "../queryParser";
import { API_URL, CHUNK_SIZE } from "../constants";
import * as UploadUtils from "../uploadUtils";
import * as UploadUtils from "../upload/uploadUtils";
const urlChangedEpic = (actions) =>
actions.pipe(

128
src/upload/UploadGrid.js Normal file
View file

@ -0,0 +1,128 @@
import React, { useMemo } from "react";
import { useSelector } from "react-redux";
import { HEADING_HEIGHT } from "../constants";
import { useActionCreator } from "../hooks";
import * as Actions from "../redux/actionCreators";
import DropTarget from "../DropTarget";
import UploadSection from "./UploadSection";
function useBucketed(filesArr) {
return useMemo(() => {
const missing = [];
const notSkins = [];
const foundSkins = [];
filesArr.forEach((file) => {
switch (file.status) {
case "MISSING":
case "UPLOADING":
case "UPLOAD_FAILED":
case "ARCHIVED":
missing.push(file);
break;
case "INVALID_FILE_EXTENSION":
case "INVALID_ARCHIVE":
notSkins.push(file);
break;
case "FOUND":
foundSkins.push(file);
break;
default:
}
});
return { missing, notSkins, foundSkins };
}, [filesArr]);
}
function Plural({ count, single, plural }) {
return count === 1 ? single : plural;
}
function Inner({ files }) {
const tryToUploadAllFiles = useActionCreator(Actions.tryToUploadAllFiles);
const filesArr = Object.values(files);
const { missing, notSkins, foundSkins } = useBucketed(filesArr);
const analyzing = filesArr.some((file) => file.status === "NEW");
if (analyzing) {
return (
<h1>
Analyzing {filesArr.length.toLocaleString()}{" "}
<Plural count={filesArr.length} single="file" plural="files" />
...
</h1>
);
}
const stillHaveFilesToUpload = filesArr.some(
(file) => file.status === "MISSING" || file.status === "UPLOADING"
);
const filesToUpload = missing.length;
const getTitle = () => {
if (filesToUpload > 0) {
if (stillHaveFilesToUpload) {
return (
<>
Found {filesToUpload.toLocaleString()}{" "}
<Plural single="skin" plural="skins" count={filesToUpload} /> to
upload
</>
);
} else {
return `Thanks for your contribution!`;
}
}
return `No missing skins found`;
};
return (
<div style={{ maxWidth: 600, minWidth: 400, lineHeight: 1.2 }}>
<h1>{getTitle()}</h1>
<UploadSection
title="Missing from the museum"
extra={
stillHaveFilesToUpload && (
<button
onClick={tryToUploadAllFiles}
style={{ marginRight: 0, height: 30, fontWeight: "bold" }}
>
Upload All
</button>
)
}
files={missing}
/>
<UploadSection title="Invalid" files={notSkins} />
<UploadSection title="Already collected" files={foundSkins} />
</div>
);
}
function UploadGrid({ getInputProps, isDragActive, ...props }) {
const files = useSelector((state) => state.fileUploads);
return (
<div
style={{
position: "absolute",
top: HEADING_HEIGHT,
left: 0,
bottom: 0,
right: 0,
display: "flex",
flexDirection: "column",
color: "lightgrey",
}}
>
{isDragActive || Object.keys(files).length === 0 ? (
<DropTarget getInputProps={getInputProps} />
) : (
<div style={{ padding: 15, display: "flex", justifyContent: "center" }}>
<Inner files={files} {...props} />
</div>
)}
</div>
);
}
export default UploadGrid;

117
src/upload/UploadRow.js Normal file
View file

@ -0,0 +1,117 @@
import React from "react";
import * as Utils from "../utils";
function Row({ name, loading, right, complete }) {
return (
<div
style={{
borderBottom: "1px solid rgba(32, 31, 51, 1)",
position: "relative",
paddingTop: 4,
paddingBottom: 2,
}}
>
{(loading != null || complete) && (
<div
style={{
backgroundColor: "rgba(51, 71, 88, 1)",
position: "absolute",
left: 0,
top: 0,
width: loading ? `90%` : complete ? `100%` : `0%`,
transitionProperty: "all",
// TODO: Try to learn how long it really takes
transitionDuration: complete ? "200ms" : "9s",
height: "100%",
zIndex: 0,
}}
/>
)}
<div
style={{
zIndex: 1,
position: "relative",
display: "flex",
justifyContent: "space-between",
}}
>
<code>{name}</code>
{right && (
<code
style={{
color: "darkgray",
paddingLeft: 10,
// Ensure we are wide enough that text changes won't affect the layout
minWidth: 100,
textAlign: "right",
}}
>
{right}
</code>
)}
</div>
</div>
);
}
function SkinLink({ md5, children }) {
return (
<a
href={Utils.museumUrlFromHash(md5)}
target="_blank"
rel="noopener noreferrer"
style={{ color: "darkgray" }}
>
{children}
</a>
);
}
// TODO: This is a component
function getRight(file) {
switch (file.status) {
case "ARCHIVED":
switch (file.skinType) {
case "MODERN":
return "archived";
case "CLASSIC":
return <SkinLink md5={file.md5}>added</SkinLink>;
default:
throw new Error(`Unknown skinType "${file.skinType}"`);
}
case "FOUND":
switch (file.skinType) {
case "MODERN":
return "archived";
case "CLASSIC":
return <SkinLink md5={file.md5}>view</SkinLink>;
default:
throw new Error(`Unknown skinType "${file.skinType}"`);
}
case "MISSING":
return "missing";
case "UPLOADING":
return "uploading...";
case "UPLOAD_FAILED":
return "upload failed";
case "INVALID_ARCHIVE":
return "corrupt";
case "INVALID_FILE_EXTENSION":
return "not skin";
default:
return file.status;
}
}
function UploadRow({ file }) {
return (
<Row
name={file.file.name}
loading={file.status === "UPLOADING"}
right={getRight(file)}
complete={file.status === "ARCHIVED"}
/>
);
}
export default UploadRow;

View file

@ -0,0 +1,34 @@
import React from "react";
import UploadRow from "./UploadRow";
function UploadSection({ files, title, extra }) {
if (files.length === 0) {
return null;
}
return (
<div
style={{
paddingBottom: 20,
}}
>
<div
style={{
display: "flex",
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
}}
>
<h2>
{title} ({files.length.toLocaleString()})
</h2>
<div>{extra}</div>
</div>
{files.map((file) => (
<UploadRow key={file.id} file={file} />
))}
</div>
);
}
export default UploadSection;

View file

@ -1,4 +1,4 @@
import { API_URL } from "./constants";
import { API_URL } from "../constants";
export async function upload(file) {
const formData = new FormData();
formData.append("skin", file, file.name);
@ -22,7 +22,7 @@ export async function checkMd5sAreMissing(md5s) {
}
export async function hashFile(file) {
const { hashFile: hasher } = await import("./hashFile");
const { hashFile: hasher } = await import("../hashFile");
return hasher(file);
}