mirror of
https://github.com/captbaritone/webamp.git
synced 2026-07-23 10:07:35 +00:00
Make the upload page its own route
This commit is contained in:
parent
ceb0675bbc
commit
2539f67145
10 changed files with 184 additions and 197 deletions
31
src/App.js
31
src/App.js
|
|
@ -10,15 +10,9 @@ import { useSelector } from "react-redux";
|
|||
import * as Selectors from "./redux/selectors";
|
||||
import * as Actions from "./redux/actionCreators";
|
||||
import { ABOUT_PAGE } from "./constants";
|
||||
import {
|
||||
useWindowSize,
|
||||
useScrollbarWidth,
|
||||
useDropFiles,
|
||||
useActionCreator,
|
||||
} from "./hooks";
|
||||
import { SCREENSHOT_WIDTH, SKIN_RATIO, SHOW_UPLOAD } from "./constants";
|
||||
import { useWindowSize, useScrollbarWidth, useActionCreator } from "./hooks";
|
||||
import { SCREENSHOT_WIDTH, SKIN_RATIO } from "./constants";
|
||||
import UploadGrid from "./UploadGrid";
|
||||
import DropTarget from "./DropTarget";
|
||||
import Metadata from "./components/Metadata";
|
||||
import SkinReadme from "./SkinReadme";
|
||||
import { useDropzone } from "react-dropzone";
|
||||
|
|
@ -50,11 +44,10 @@ function App(props) {
|
|||
},
|
||||
[gotFiles]
|
||||
);
|
||||
const {
|
||||
getRootProps,
|
||||
getInputProps,
|
||||
isDragActive: areDragging,
|
||||
} = useDropzone({ onDrop });
|
||||
const { getRootProps, getInputProps, isDragActive } = useDropzone({
|
||||
onDrop,
|
||||
noClick: true,
|
||||
});
|
||||
|
||||
const fileExplorerOpen = useSelector(Selectors.getFileExplorerOpen);
|
||||
|
||||
|
|
@ -62,10 +55,8 @@ function App(props) {
|
|||
<div {...getRootProps()}>
|
||||
<Head />
|
||||
<Header />
|
||||
{areDragging && SHOW_UPLOAD ? (
|
||||
<DropTarget getInputProps={getInputProps} />
|
||||
) : props.uploadViewOpen ? (
|
||||
<UploadGrid />
|
||||
{props.uploadViewOpen || isDragActive ? (
|
||||
<UploadGrid isDragActive={isDragActive} getInputProps={getInputProps} />
|
||||
) : (
|
||||
<SkinTable
|
||||
columnCount={columnCount}
|
||||
|
|
@ -75,7 +66,7 @@ function App(props) {
|
|||
windowWidth={windowWidthWithScrollabar}
|
||||
/>
|
||||
)}
|
||||
{props.aboutPage ? (
|
||||
{props.page === ABOUT_PAGE ? (
|
||||
<Overlay>
|
||||
<About />
|
||||
</Overlay>
|
||||
|
|
@ -100,9 +91,9 @@ function App(props) {
|
|||
const mapStateToProps = (state) => ({
|
||||
selectedSkinHash: Selectors.getSelectedSkinHash(state),
|
||||
overlayShouldAnimate: Selectors.overlayShouldAnimate(state),
|
||||
aboutPage: Selectors.getActiveContentPage(state) === ABOUT_PAGE,
|
||||
page: Selectors.getActiveContentPage(state),
|
||||
scale: state.scale,
|
||||
uploadViewOpen: Selectors.getHaveUploadFiles(state),
|
||||
uploadViewOpen: Selectors.getUploadViewOpen(state),
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps)(App);
|
||||
|
|
|
|||
|
|
@ -1,38 +0,0 @@
|
|||
import React from "react";
|
||||
import { HEADING_HEIGHT } from "./constants";
|
||||
|
||||
function DropTarget({ getInputProps }) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
backgroundColor: "rgba(0, 0, 0, 0.8)",
|
||||
top: HEADING_HEIGHT,
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
display: "flex",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
margin: 20,
|
||||
flexGrow: 1,
|
||||
border: "8px dashed #FFF",
|
||||
borderRadius: 20,
|
||||
color: "grey",
|
||||
textAlign: "center",
|
||||
vericalAlign: "middle",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
flexDirection: "column",
|
||||
fontSize: 30,
|
||||
}}
|
||||
>
|
||||
Drop Skins Here
|
||||
<input {...getInputProps} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export default DropTarget;
|
||||
184
src/Header.js
184
src/Header.js
|
|
@ -1,10 +1,9 @@
|
|||
import * as React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import * as Utils from "./utils";
|
||||
import * as Selectors from "./redux/selectors";
|
||||
import * as Actions from "./redux/actionCreators";
|
||||
import Disposable from "./Disposable";
|
||||
import { useWindowSize } from "./hooks";
|
||||
import { useActionCreator, useWindowSize } from "./hooks";
|
||||
import { ReactComponent as AlgoliaLogo } from "./searchByAlgoliaDarkbBackground.svg";
|
||||
import algoliaLogoSmallUrl from "./searchByAlgoliaSmall.png";
|
||||
import UploadButton from "./UploadButton";
|
||||
|
|
@ -23,67 +22,73 @@ function SearchLogo() {
|
|||
);
|
||||
}
|
||||
|
||||
class Header extends React.Component {
|
||||
constructor(props) {
|
||||
super();
|
||||
this._disposable = new Disposable();
|
||||
this._inputRef = null;
|
||||
}
|
||||
function useFocusOnSlash() {
|
||||
const [input, setInput] = useState(null);
|
||||
|
||||
componentDidMount() {
|
||||
useEffect(() => {
|
||||
if (input == null) {
|
||||
return;
|
||||
}
|
||||
const handler = (e) => {
|
||||
// slash
|
||||
if (e.keyCode === 191) {
|
||||
if (this._inputRef == null) {
|
||||
return;
|
||||
}
|
||||
if (this._inputRef !== document.activeElement) {
|
||||
this._inputRef.focus();
|
||||
if (input !== document.activeElement) {
|
||||
input.focus();
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
};
|
||||
window.document.addEventListener("keydown", handler);
|
||||
this._disposable.add(() => {
|
||||
return () => {
|
||||
window.document.removeEventListener("keydown", handler);
|
||||
});
|
||||
}
|
||||
};
|
||||
}, [input]);
|
||||
|
||||
componentWillUnmount() {
|
||||
this._disposable.dispose();
|
||||
}
|
||||
render() {
|
||||
return (
|
||||
<div id="search">
|
||||
<h1>
|
||||
return setInput;
|
||||
}
|
||||
|
||||
function Header() {
|
||||
const searchQuery = useSelector(Selectors.getSearchQuery);
|
||||
// const scale = useSelector((state) => state.scale);
|
||||
const uploadViewOpen = useSelector(Selectors.getUploadViewOpen);
|
||||
|
||||
const setSearchQuery = useActionCreator(Actions.searchQueryChanged);
|
||||
const requestRandomSkin = useActionCreator(Actions.requestedRandomSkin);
|
||||
const requestedAboutPage = useActionCreator(Actions.requestedAboutPage);
|
||||
// const setScale = useActionCreator((scale) => ({ type: "SET_SCALE", scale }));
|
||||
const setInput = useFocusOnSlash();
|
||||
|
||||
return (
|
||||
<div id="search">
|
||||
<h1>
|
||||
<a
|
||||
href="/"
|
||||
onClick={(e) => {
|
||||
if (Utils.eventIsLinkClick(e)) {
|
||||
e.preventDefault();
|
||||
setSearchQuery(null);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span id="logo">{"🌩️"}</span>
|
||||
<span className="name">Winamp Skin Museum</span>
|
||||
</a>
|
||||
</h1>
|
||||
<span style={{ flexGrow: 1 }} />
|
||||
{uploadViewOpen || (
|
||||
<>
|
||||
<a
|
||||
href="/"
|
||||
onClick={(e) => {
|
||||
if (Utils.eventIsLinkClick(e)) {
|
||||
e.preventDefault();
|
||||
this.props.setSearchQuery(null);
|
||||
}
|
||||
href="https://www.algolia.com/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={{
|
||||
opacity: searchQuery ? 0.5 : 0,
|
||||
transition: "opacity ease-in 300ms",
|
||||
}}
|
||||
>
|
||||
<span id="logo">{"🌩️"}</span>
|
||||
<span className="name">Winamp Skin Museum</span>
|
||||
<SearchLogo />
|
||||
</a>
|
||||
</h1>
|
||||
<span style={{ flexGrow: 1 }} />
|
||||
{this.props.uploadViewOpen || (
|
||||
<>
|
||||
<a
|
||||
href="https://www.algolia.com/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={{
|
||||
opacity: this.props.searchQuery ? 0.5 : 0,
|
||||
transition: "opacity ease-in 300ms",
|
||||
}}
|
||||
>
|
||||
<SearchLogo />
|
||||
</a>
|
||||
{/*
|
||||
{/*
|
||||
<button
|
||||
onClick={() => {
|
||||
this.props.setScale(this.props.scale + 0.1);
|
||||
|
|
@ -99,56 +104,33 @@ class Header extends React.Component {
|
|||
-
|
||||
</button>
|
||||
*/}
|
||||
<input
|
||||
type="search"
|
||||
style={{ marginLeft: 10 }}
|
||||
onChange={(e) => this.props.setSearchQuery(e.target.value)}
|
||||
value={this.props.searchQuery || ""}
|
||||
placeholder={"Search..."}
|
||||
ref={(node) => {
|
||||
this._inputRef = node;
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
onClick={() => {
|
||||
this.props.requestRandomSkin();
|
||||
}}
|
||||
>
|
||||
Random
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
this.props.requestedAboutPage();
|
||||
}}
|
||||
>
|
||||
?
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
<UploadButton />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
<input
|
||||
type="search"
|
||||
style={{ marginLeft: 10 }}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
value={searchQuery || ""}
|
||||
placeholder={"Search..."}
|
||||
ref={setInput}
|
||||
/>
|
||||
<button
|
||||
onClick={() => {
|
||||
requestRandomSkin();
|
||||
}}
|
||||
>
|
||||
Random
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
requestedAboutPage();
|
||||
}}
|
||||
>
|
||||
?
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
<UploadButton />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
searchQuery: Selectors.getSearchQuery(state),
|
||||
scale: state.scale,
|
||||
uploadViewOpen: Selectors.getHaveUploadFiles(state),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
setSearchQuery(query) {
|
||||
dispatch(Actions.searchQueryChanged(query));
|
||||
},
|
||||
requestRandomSkin() {
|
||||
dispatch(Actions.requestedRandomSkin());
|
||||
},
|
||||
requestedAboutPage() {
|
||||
dispatch(Actions.requestedAboutPage());
|
||||
},
|
||||
setScale(scale) {
|
||||
dispatch({ type: "SET_SCALE", scale });
|
||||
},
|
||||
});
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(Header);
|
||||
export default Header;
|
||||
|
|
|
|||
|
|
@ -5,27 +5,28 @@ import * as Actions from "./redux/actionCreators";
|
|||
import { SHOW_UPLOAD } from "./constants";
|
||||
import UploadIcon from "./components/icons/UploadIcon";
|
||||
import CloseIcon from "./components/icons/CloseIcon";
|
||||
import { promptForFileReferences } from "./utils";
|
||||
import * as Selectors from "./redux/selectors";
|
||||
|
||||
function UploadButton() {
|
||||
const uploadViewOpen = useSelector(Selectors.getHaveUploadFiles);
|
||||
const gotFiles = useActionCreator(Actions.gotFiles);
|
||||
const uploadViewOpen = useSelector(Selectors.getUploadViewOpen);
|
||||
const closeUploadFiles = useActionCreator(Actions.closeUploadFiles);
|
||||
const requestedUploadPage = useActionCreator(Actions.requestedUploadPage);
|
||||
|
||||
if (!SHOW_UPLOAD) {
|
||||
return null;
|
||||
// return null;
|
||||
}
|
||||
|
||||
const style = { paddingLeft: "0.2rem", paddingRight: "0.2rem" };
|
||||
const style = {
|
||||
paddingLeft: "0.2rem",
|
||||
paddingRight: "0.2rem",
|
||||
};
|
||||
|
||||
// TODO: Make these buttons links.
|
||||
if (uploadViewOpen) {
|
||||
return (
|
||||
<button
|
||||
onClick={() => {
|
||||
const areSure = window.confirm("Are you sure you're done uploading?");
|
||||
if (areSure) {
|
||||
closeUploadFiles();
|
||||
}
|
||||
closeUploadFiles();
|
||||
}}
|
||||
style={style}
|
||||
>
|
||||
|
|
@ -35,11 +36,9 @@ function UploadButton() {
|
|||
} else {
|
||||
return (
|
||||
<button
|
||||
onClick={async () => {
|
||||
const fileList = await promptForFileReferences({
|
||||
accept: ".wsz,.zip",
|
||||
});
|
||||
gotFiles(Array.from(fileList));
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
requestedUploadPage();
|
||||
}}
|
||||
style={style}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -58,27 +58,62 @@ function UploadRow({ file }) {
|
|||
);
|
||||
}
|
||||
|
||||
function UploadGrid() {
|
||||
function UploadGrid({ getInputProps, isDragActive }) {
|
||||
const files = useSelector((state) => state.fileUploads);
|
||||
const tryToUploadAllFiles = useActionCreator(Actions.tryToUploadAllFiles);
|
||||
const canUpload = useSelector(Selectors.getFileToUpload) != null;
|
||||
return (
|
||||
<div style={{ color: "white", marginTop: HEADING_HEIGHT }}>
|
||||
{canUpload && <button onClick={tryToUploadAllFiles}>Upload All</button>}
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Action</th>
|
||||
<th>Filename</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{Object.values(files).map((file, i) => {
|
||||
return <UploadRow key={i} file={file} />;
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
backgroundColor: "rgba(0, 0, 0, 0.8)",
|
||||
top: HEADING_HEIGHT,
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
display: "flex",
|
||||
}}
|
||||
>
|
||||
{isDragActive || Object.keys(files).length === 0 ? (
|
||||
<div
|
||||
style={{
|
||||
margin: 20,
|
||||
flexGrow: 1,
|
||||
border: "8px dashed #FFF",
|
||||
borderRadius: 20,
|
||||
color: "grey",
|
||||
textAlign: "center",
|
||||
vericalAlign: "middle",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
flexDirection: "column",
|
||||
fontSize: 30,
|
||||
}}
|
||||
>
|
||||
Drop Skins Here
|
||||
<input {...getInputProps()} />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{canUpload && (
|
||||
<button onClick={tryToUploadAllFiles}>Upload All</button>
|
||||
)}
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Action</th>
|
||||
<th>Filename</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{Object.values(files).map((file, i) => {
|
||||
return <UploadRow key={i} file={file} />;
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ export const SCREENSHOT_WIDTH = 275;
|
|||
export const SCREENSHOT_HEIGHT = 348;
|
||||
export const SKIN_RATIO = SCREENSHOT_HEIGHT / SCREENSHOT_WIDTH;
|
||||
export const ABOUT_PAGE = "ABOUT_PAGE";
|
||||
export const UPLOAD_PAGE = "UPLOAD_PAGE";
|
||||
// export const CDN = "https://s3.amazonaws.com/webamp-uploaded-skins";
|
||||
// export const CDN = "https://s3.amazonaws.com/cdn.webampskins.org";
|
||||
export const SCREENSHOT_CDN = "https://cdn.webampskins.org";
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { ABOUT_PAGE, UPLOAD_PAGE } from "../constants";
|
||||
export function closeModal() {
|
||||
return { type: "CLOSE_MODAL" };
|
||||
}
|
||||
|
|
@ -109,7 +110,11 @@ export function gotFocusedSkinFile(content) {
|
|||
}
|
||||
|
||||
export function requestedAboutPage() {
|
||||
return { type: "REQUESTED_ABOUT_PAGE" };
|
||||
return { type: "REQUESTED_PAGE", page: ABOUT_PAGE };
|
||||
}
|
||||
|
||||
export function requestedUploadPage() {
|
||||
return { type: "REQUESTED_PAGE", page: UPLOAD_PAGE };
|
||||
}
|
||||
|
||||
export function selectRelativeSkin(offset) {
|
||||
|
|
|
|||
|
|
@ -27,6 +27,9 @@ const urlChangedEpic = (actions) =>
|
|||
if (action.location.pathname === "/about/") {
|
||||
return of(Actions.requestedAboutPage());
|
||||
}
|
||||
if (action.location.pathname === "/upload/") {
|
||||
return of(Actions.requestedUploadPage());
|
||||
}
|
||||
const params = new URLSearchParams(action.location.search);
|
||||
const query = params != null && params.get("query");
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { ABOUT_PAGE, CHUNK_SIZE } from "../constants";
|
||||
import { CHUNK_SIZE, UPLOAD_PAGE } from "../constants";
|
||||
|
||||
const defaultState = {
|
||||
searchQuery: null,
|
||||
|
|
@ -26,6 +26,9 @@ export default function reducer(state = defaultState, action) {
|
|||
case "SET_SCALE": {
|
||||
return { ...state, scale: action.scale };
|
||||
}
|
||||
case "GOT_FILES": {
|
||||
return { ...state, activeContentPage: UPLOAD_PAGE };
|
||||
}
|
||||
case "GOT_FILE": {
|
||||
return {
|
||||
...state,
|
||||
|
|
@ -141,6 +144,7 @@ export default function reducer(state = defaultState, action) {
|
|||
case "CLOSE_UPLOAD_FILES": {
|
||||
return {
|
||||
...state,
|
||||
activeContentPage: null,
|
||||
fileUploads: {},
|
||||
};
|
||||
}
|
||||
|
|
@ -248,10 +252,11 @@ export default function reducer(state = defaultState, action) {
|
|||
content: action.content,
|
||||
},
|
||||
};
|
||||
case "REQUESTED_ABOUT_PAGE":
|
||||
case "REQUESTED_PAGE":
|
||||
console.log(action);
|
||||
return {
|
||||
...state,
|
||||
activeContentPage: ABOUT_PAGE,
|
||||
activeContentPage: action.page,
|
||||
};
|
||||
case "OPEN_FILE_EXPLORER":
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { createSelector } from "reselect";
|
||||
import * as Utils from "../utils";
|
||||
import { ABOUT_PAGE } from "../constants";
|
||||
import { ABOUT_PAGE, UPLOAD_PAGE } from "../constants";
|
||||
|
||||
export function getSelectedSkinHash(state) {
|
||||
return state.selectedSkinHash;
|
||||
|
|
@ -47,8 +47,9 @@ export const getFileToUpload = (state) => {
|
|||
);
|
||||
};
|
||||
|
||||
export const getHaveUploadFiles = (state) =>
|
||||
Object.keys(state.fileUploads).length > 0;
|
||||
export const getUploadViewOpen = (state) => {
|
||||
return state.activeContentPage === UPLOAD_PAGE;
|
||||
};
|
||||
|
||||
export const getAreDragging = (state) => state.areDragging;
|
||||
|
||||
|
|
@ -145,6 +146,9 @@ export const getUrl = createSelector(
|
|||
if (activeContentPage === ABOUT_PAGE) {
|
||||
return "/about/";
|
||||
}
|
||||
if (activeContentPage === UPLOAD_PAGE) {
|
||||
return "/upload/";
|
||||
}
|
||||
if (hash) {
|
||||
const skinUrl = getPermalinkUrlFromHash(hash);
|
||||
if (fileExplorerOpen && focusedSkinFile) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue