diff --git a/src/App.js b/src/App.js index ec74f68c..14ec30f6 100644 --- a/src/App.js +++ b/src/App.js @@ -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"; diff --git a/src/DropTarget.js b/src/DropTarget.js new file mode 100644 index 00000000..b305a8b2 --- /dev/null +++ b/src/DropTarget.js @@ -0,0 +1,39 @@ +import React from "react"; + +function DropTarget({ getInputProps }) { + return ( +
+
Drop Skins Here
+
+ We'll analyze them in your browser to find any that are missing from the + museum +
+ +
+ ); +} + +export default DropTarget; diff --git a/src/UploadGrid.js b/src/UploadGrid.js deleted file mode 100644 index 10dca643..00000000 --- a/src/UploadGrid.js +++ /dev/null @@ -1,173 +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"; - -function DropTarget({ getInputProps }) { - return ( -
-
Drop Skins Here
-
- We'll analyze them in your browser to find any that are missing from the - museum -
- -
- ); -} - -function Section({ files, filter, title, render }) { - const matches = useMemo(() => files.filter(filter), [files, filter]); - if (matches.length === 0) { - return null; - } - return ( - <> -

- {matches.length.toLocaleString()} {title} -

- - - ); -} - -function UploadGrid({ getInputProps, isDragActive }) { - const files = useSelector((state) => state.fileUploads); - const tryToUploadAllFiles = useActionCreator(Actions.tryToUploadAllFiles); - const filesArr = Object.values(files); - - return ( -
- {isDragActive || Object.keys(files).length === 0 ? ( - - ) : ( -
-
- This feature is still in beta. If you have any issues, please reach - out to{" "} - - jordan@jordaneldredge.com - - . -
-

- {`You've dragged in ${filesArr.length.toLocaleString()} files`} - {filesArr.some((file) => file.status === "NEW") && - " (Analyzing...)"} -

-
- are new skins! - {filesArr.some((file) => file.status === "MISSING") && ( -
- -
- )} - - } - filter={(file) => - file.status === "MISSING" || - file.status === "UPLOADING" || - file.status === "UPLOAD_FAILED" || - file.status === "ARCHIVED" - } - render={(file) => { - switch (file.status) { - case "MISSING": - return file.file.name; - case "UPLOADING": - return <>{file.file.name} (🚀 Uploading...); - case "UPLOAD_FAILED": - return <>{file.file.name} (❌ Upload Failed); - case "ARCHIVED": - return ( - <> - - {file.file.name} - {" "} - (✅ Added!) - - ); - default: - console.error(`Unexpected file status: ${file.status}`); - return null; - } - }} - /> -
file.status === "INVALID_FILE_EXTENSION"} - render={(file) => file.file.name} - /> -
file.status === "NOT_CLASSIC_SKIN"} - render={(file) => file.file.name} - /> -
file.status === "FOUND"} - render={(file) => ( - - {file.file.name} - - )} - /> -
- )} -
- ); -} -export default UploadGrid; diff --git a/src/components/Metadata.js b/src/components/Metadata.js index dfc26651..2a6252a1 100644 --- a/src/components/Metadata.js +++ b/src/components/Metadata.js @@ -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"; diff --git a/src/redux/actionCreators.js b/src/redux/actionCreators.js index 1bd1d3d1..44e94fd3 100644 --- a/src/redux/actionCreators.js +++ b/src/redux/actionCreators.js @@ -67,8 +67,12 @@ export function invalidFileExtension(id) { return { type: "INVALID_FILE_EXTENSION", id }; } -export function notClassicSkin(id) { - return { type: "NOT_CLASSIC_SKIN", id }; +export function invalidArchive(id) { + return { type: "INVALID_ARCHIVE", id }; +} + +export function gotSkinType(id, skinType) { + return { type: "GOT_SKIN_TYPE", id, skinType }; } export function gotFileMd5(id, md5) { diff --git a/src/redux/epics.js b/src/redux/epics.js index c3f4494d..d43bf57e 100644 --- a/src/redux/epics.js +++ b/src/redux/epics.js @@ -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( @@ -213,16 +213,17 @@ const uploadSingleFileEpic = (actions) => if (!UploadUtils.isValidSkinFilename(file.name)) { return of(Actions.invalidFileExtension(id)); } - return from(UploadUtils.isClassicSkin(file)) + return from(UploadUtils.getSkinType(file)) .pipe( - mergeMap((isClassic) => { - if (!isClassic) { - return of(Actions.notClassicSkin(id)); + mergeMap((skinType) => { + if (skinType === null) { + return of(Actions.invalidArchive(id)); } - return from(UploadUtils.hashFile(file)).pipe( - map((md5) => { - return Actions.gotFileMd5(id, md5); - }) + return concat( + of(Actions.gotSkinType(id, skinType)), + from(UploadUtils.hashFile(file)).pipe( + map((md5) => Actions.gotFileMd5(id, md5)) + ) ); }) ) @@ -235,8 +236,8 @@ const checkIfUploadsAreMissingEpic = (actions, state) => filter((action) => { return ( (action.type === "GOT_FILE_MD5" || - action.type === "NOT_CLASSIC_SKIN" || - action.type === "INVALID_FILE_EXTENSION") && + action.type === "INVALID_FILE_EXTENSION" || + action.type === "INVALID_ARCHIVE") && Selectors.getAreReadyToCheckMissingUploads(state.value) ); }), @@ -323,7 +324,6 @@ const loggingEpic = (actions, state) => case "TRY_TO_UPLOAD_ALL_FILES": case "INVALID_FILE_EXTENSION": case "GOT_FILE": - case "NOT_CLASSIC_SKIN": case "GOT_MISSING_AND_FOUND_MD5S": window.ga("send", "event", "redux", action.type); break; diff --git a/src/redux/reducer.js b/src/redux/reducer.js index a74a29e7..ef569fe5 100644 --- a/src/redux/reducer.js +++ b/src/redux/reducer.js @@ -66,8 +66,18 @@ export default function reducer(state = defaultState, action) { true ); } - case "NOT_CLASSIC_SKIN": { - return setUploadFileStatus(state, action.id, "NOT_CLASSIC_SKIN", true); + case "GOT_SKIN_TYPE": { + const previousFile = state.fileUploads[action.id]; + return { + ...state, + fileUploads: { + ...state.fileUploads, + [action.id]: { + ...previousFile, + skinType: action.skinType, + }, + }, + }; } case "STARTING_FILE_UPLOAD": { return setUploadFileStatus(state, action.id, "UPLOADING"); @@ -78,6 +88,8 @@ export default function reducer(state = defaultState, action) { case "ARCHIVED_SKIN": { return setUploadFileStatus(state, action.id, "ARCHIVED"); } + case "INVALID_ARCHIVE": + return setUploadFileStatus(state, action.id, "INVALID_ARCHIVE"); case "GOT_FILE_MD5": { return { ...state, @@ -232,7 +244,6 @@ export default function reducer(state = defaultState, action) { }, }; case "REQUESTED_PAGE": - console.log(action); return { ...state, activeContentPage: action.page, diff --git a/src/upload/UploadGrid.js b/src/upload/UploadGrid.js new file mode 100644 index 00000000..513a7630 --- /dev/null +++ b/src/upload/UploadGrid.js @@ -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 ( +

+ Analyzing {filesArr.length.toLocaleString()}{" "} + + ... +

+ ); + } + + 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()}{" "} + to + upload + + ); + } else { + return `Thanks for your contribution!`; + } + } + return `No missing skins found`; + }; + + return ( +
+

{getTitle()}

+ + Upload All + + ) + } + files={missing} + /> + + +
+ ); +} + +function UploadGrid({ getInputProps, isDragActive, ...props }) { + const files = useSelector((state) => state.fileUploads); + return ( +
+ {isDragActive || Object.keys(files).length === 0 ? ( + + ) : ( +
+ +
+ )} +
+ ); +} +export default UploadGrid; diff --git a/src/upload/UploadRow.js b/src/upload/UploadRow.js new file mode 100644 index 00000000..aab6e526 --- /dev/null +++ b/src/upload/UploadRow.js @@ -0,0 +1,117 @@ +import React from "react"; +import * as Utils from "../utils"; + +function Row({ name, loading, right, complete }) { + return ( +
+ {(loading != null || complete) && ( +
+ )} +
+ {name} + {right && ( + + {right} + + )} +
+
+ ); +} + +function SkinLink({ md5, children }) { + return ( + + {children} + + ); +} + +// TODO: This is a component +function getRight(file) { + switch (file.status) { + case "ARCHIVED": + switch (file.skinType) { + case "MODERN": + return "archived"; + case "CLASSIC": + return added; + default: + throw new Error(`Unknown skinType "${file.skinType}"`); + } + case "FOUND": + switch (file.skinType) { + case "MODERN": + return "archived"; + case "CLASSIC": + return view; + 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 ( + + ); +} + +export default UploadRow; diff --git a/src/upload/UploadSection.js b/src/upload/UploadSection.js new file mode 100644 index 00000000..feffb179 --- /dev/null +++ b/src/upload/UploadSection.js @@ -0,0 +1,34 @@ +import React from "react"; +import UploadRow from "./UploadRow"; + +function UploadSection({ files, title, extra }) { + if (files.length === 0) { + return null; + } + return ( +
+
+

+ {title} ({files.length.toLocaleString()}) +

+
{extra}
+
+ {files.map((file) => ( + + ))} +
+ ); +} + +export default UploadSection; diff --git a/src/uploadUtils.js b/src/upload/uploadUtils.js similarity index 75% rename from src/uploadUtils.js rename to src/upload/uploadUtils.js index 2b5bbb0a..c06187ef 100644 --- a/src/uploadUtils.js +++ b/src/upload/uploadUtils.js @@ -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); } @@ -33,14 +33,19 @@ export function isValidSkinFilename(filename) { return validSkinFilename.test(filename); } -export async function isClassicSkin(file) { +export async function getSkinType(file) { const JSZip = await import("jszip"); try { const zip = await JSZip.loadAsync(file); - return zip.file(/main\.bmp$/i).length > 0; + if (zip.file(/main\.bmp$/i).length > 0) { + return "CLASSIC"; + } else if (zip.file(/skin\.xml$/i).length > 0) { + return "MODERN"; + } + return null; } catch (e) { // TODO: We could give a better message here. console.error(e); - return false; + return null; } }