ALL THE THIGNS KLSJDFLKDJSFLSDF

This commit is contained in:
Derek Schmidt 2020-12-31 11:53:16 -07:00
parent 8760f61f65
commit 72691532e7
32 changed files with 1331 additions and 25 deletions

205
package-lock.json generated
View file

@ -1821,6 +1821,24 @@
}
}
},
"@reduxjs/toolkit": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.5.0.tgz",
"integrity": "sha512-E/FUraRx+8guw9Hlg/Ja8jI/hwCrmIKed8Annt9YsZw3BQp+F24t5I5b2OWR6pkEHY4hn1BgP08FrTZFRKsdaQ==",
"requires": {
"immer": "^8.0.0",
"redux": "^4.0.0",
"redux-thunk": "^2.3.0",
"reselect": "^4.0.0"
},
"dependencies": {
"immer": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/immer/-/immer-8.0.0.tgz",
"integrity": "sha512-jm87NNBAIG4fHwouilCHIecFXp5rMGkiFrAuhVO685UnMAlOneEAnOyzPt8OnP47TC11q/E7vpzZe0WvwepFTg=="
}
}
},
"@rollup/plugin-node-resolve": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-7.1.3.tgz",
@ -2988,6 +3006,11 @@
"resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz",
"integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg=="
},
"attr-accept": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.2.tgz",
"integrity": "sha512-7prDjvt9HmqiZ0cl5CRjtS84sEyhsHP2coDkaZKRKVfCDo9s7iw7ChVmar78Gu9pC4SoR/28wFu/G5JJhTnqEg=="
},
"autoprefixer": {
"version": "9.8.6",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-9.8.6.tgz",
@ -3017,6 +3040,14 @@
"resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.1.1.tgz",
"integrity": "sha512-5Kgy8Cz6LPC9DJcNb3yjAXTu3XihQgEdnIg50c//zOC/MyLP0Clg+Y8Sh9ZjjnvBrDZU4DgXS9C3T9r4/scGZQ=="
},
"axios": {
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz",
"integrity": "sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==",
"requires": {
"follow-redirects": "^1.10.0"
}
},
"axobject-query": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz",
@ -4745,6 +4776,11 @@
}
}
},
"csstype": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.5.tgz",
"integrity": "sha512-uVDi8LpBUKQj6sdxNaTetL6FpeCqTjOvAQuQUa/qAqq8oOd4ivkbhgnqayl0dnPal8Tb/yB1tF+gOvCBiicaiQ=="
},
"cyclist": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/cyclist/-/cyclist-1.0.1.tgz",
@ -5074,6 +5110,15 @@
"utila": "~0.4"
}
},
"dom-helpers": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.0.tgz",
"integrity": "sha512-Ru5o9+V8CpunKnz5LGgWXkmrH/20cGKwcHwS4m73zIvs54CN9epEmT/HLqFJW3kXpakAFkEdzgy1hzlJe3E4OQ==",
"requires": {
"@babel/runtime": "^7.8.7",
"csstype": "^3.0.2"
}
},
"dom-serializer": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.2.tgz",
@ -6480,6 +6525,21 @@
}
}
},
"file-selector": {
"version": "0.2.4",
"resolved": "https://registry.npmjs.org/file-selector/-/file-selector-0.2.4.tgz",
"integrity": "sha512-ZDsQNbrv6qRi1YTDOEWzf5J2KjZ9KMI1Q2SGeTkCJmNNW25Jg4TW4UMcmoqcg4WrAyKRcpBXdbWRxkfrOzVRbA==",
"requires": {
"tslib": "^2.0.3"
},
"dependencies": {
"tslib": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.3.tgz",
"integrity": "sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ=="
}
}
},
"filesize": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/filesize/-/filesize-6.1.0.tgz",
@ -7113,6 +7173,19 @@
"resolved": "https://registry.npmjs.org/hex-color-regex/-/hex-color-regex-1.1.0.tgz",
"integrity": "sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ=="
},
"history": {
"version": "4.10.1",
"resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz",
"integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==",
"requires": {
"@babel/runtime": "^7.1.2",
"loose-envify": "^1.2.0",
"resolve-pathname": "^3.0.0",
"tiny-invariant": "^1.0.2",
"tiny-warning": "^1.0.0",
"value-equal": "^1.0.1"
}
},
"hmac-drbg": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz",
@ -9987,6 +10060,15 @@
"resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
"integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg=="
},
"mini-create-react-context": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/mini-create-react-context/-/mini-create-react-context-0.4.1.tgz",
"integrity": "sha512-YWCYEmd5CQeHGSAKrYvXgmzzkrvssZcuuQDDeqkT+PziKGMgE+0MCCtcKbROzocGBG1meBLl2FotlRwf4gAzbQ==",
"requires": {
"@babel/runtime": "^7.12.1",
"tiny-warning": "^1.0.3"
}
},
"mini-css-extract-plugin": {
"version": "0.11.3",
"resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-0.11.3.tgz",
@ -12407,6 +12489,16 @@
"scheduler": "^0.20.1"
}
},
"react-dropzone": {
"version": "11.2.4",
"resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-11.2.4.tgz",
"integrity": "sha512-EGSvK2CxFTuc28WxwuJCICyuYFX8b+sRumwU6Bs6sTbElV2HtQkT0d6C+HEee6XfbjiLIZ+Th9uji27rvo2wGw==",
"requires": {
"attr-accept": "^2.2.1",
"file-selector": "^0.2.2",
"prop-types": "^15.7.2"
}
},
"react-error-overlay": {
"version": "6.0.8",
"resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.8.tgz",
@ -12417,11 +12509,69 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
},
"react-redux": {
"version": "7.2.2",
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.2.tgz",
"integrity": "sha512-8+CQ1EvIVFkYL/vu6Olo7JFLWop1qRUeb46sGtIMDCSpgwPQq8fPLpirIB0iTqFe9XYEFPHssdX8/UwN6pAkEA==",
"requires": {
"@babel/runtime": "^7.12.1",
"hoist-non-react-statics": "^3.3.2",
"loose-envify": "^1.4.0",
"prop-types": "^15.7.2",
"react-is": "^16.13.1"
}
},
"react-refresh": {
"version": "0.8.3",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.8.3.tgz",
"integrity": "sha512-X8jZHc7nCMjaCqoU+V2I0cOhNW+QMBwSUkeXnTi8IPe6zaRWfn60ZzvFDZqWPfmSJfjub7dDW1SP0jaHWLu/hg=="
},
"react-router": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-5.2.0.tgz",
"integrity": "sha512-smz1DUuFHRKdcJC0jobGo8cVbhO3x50tCL4icacOlcwDOEQPq4TMqwx3sY1TP+DvtTgz4nm3thuo7A+BK2U0Dw==",
"requires": {
"@babel/runtime": "^7.1.2",
"history": "^4.9.0",
"hoist-non-react-statics": "^3.1.0",
"loose-envify": "^1.3.1",
"mini-create-react-context": "^0.4.0",
"path-to-regexp": "^1.7.0",
"prop-types": "^15.6.2",
"react-is": "^16.6.0",
"tiny-invariant": "^1.0.2",
"tiny-warning": "^1.0.0"
},
"dependencies": {
"isarray": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
"integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8="
},
"path-to-regexp": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz",
"integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==",
"requires": {
"isarray": "0.0.1"
}
}
}
},
"react-router-dom": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.2.0.tgz",
"integrity": "sha512-gxAmfylo2QUjcwxI63RhQ5G85Qqt4voZpUXSEqCwykV0baaOTQDR1f0PmY8AELqIyVc0NEZUj0Gov5lNGcXgsA==",
"requires": {
"@babel/runtime": "^7.1.2",
"history": "^4.9.0",
"loose-envify": "^1.3.1",
"prop-types": "^15.6.2",
"react-router": "5.2.0",
"tiny-invariant": "^1.0.2",
"tiny-warning": "^1.0.0"
}
},
"react-scripts": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-4.0.1.tgz",
@ -12488,6 +12638,17 @@
"workbox-webpack-plugin": "5.1.4"
}
},
"react-transition-group": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.1.tgz",
"integrity": "sha512-Djqr7OQ2aPUiYurhPalTrVy9ddmFCCzwhqQmtN+J3+3DzLO209Fdr70QrN8Z3DsglWql6iY1lDWAfpFiBtuKGw==",
"requires": {
"@babel/runtime": "^7.5.5",
"dom-helpers": "^5.0.1",
"loose-envify": "^1.4.0",
"prop-types": "^15.6.2"
}
},
"read-pkg": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz",
@ -12603,6 +12764,20 @@
"strip-indent": "^3.0.0"
}
},
"redux": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/redux/-/redux-4.0.5.tgz",
"integrity": "sha512-VSz1uMAH24DM6MF72vcojpYPtrTUu3ByVWfPL1nPfVRb5mZVTve5GnNCUV53QM/BZ66xfWrm0CTWoM+Xlz8V1w==",
"requires": {
"loose-envify": "^1.4.0",
"symbol-observable": "^1.2.0"
}
},
"redux-thunk": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.3.0.tgz",
"integrity": "sha512-km6dclyFnmcvxhAcrQV2AkZmPQjzPDjgVlQtR0EQjxZPyJ0BnMf3in1ryuR8A2qU0HldVRfxYXbFSKlI3N7Slw=="
},
"regenerate": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz",
@ -12869,6 +13044,11 @@
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
"integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8="
},
"reselect": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/reselect/-/reselect-4.0.0.tgz",
"integrity": "sha512-qUgANli03jjAyGlnbYVAV5vvnOmJnODyABz51RdBN7M4WaVu8mecZWgyQNkG8Yqe3KRGRt0l4K4B3XVEULC4CA=="
},
"resolve": {
"version": "1.18.1",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.18.1.tgz",
@ -12898,6 +13078,11 @@
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
"integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="
},
"resolve-pathname": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-3.0.0.tgz",
"integrity": "sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng=="
},
"resolve-url": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz",
@ -14317,6 +14502,11 @@
"util.promisify": "~1.0.0"
}
},
"symbol-observable": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz",
"integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ=="
},
"symbol-tree": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
@ -14576,6 +14766,16 @@
"resolved": "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz",
"integrity": "sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q="
},
"tiny-invariant": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.1.0.tgz",
"integrity": "sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw=="
},
"tiny-warning": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz",
"integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA=="
},
"tmpl": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.4.tgz",
@ -15046,6 +15246,11 @@
"spdx-expression-parse": "^3.0.0"
}
},
"value-equal": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz",
"integrity": "sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw=="
},
"vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",

View file

@ -3,15 +3,21 @@
"version": "2021.0.0",
"private": true,
"dependencies": {
"@reduxjs/toolkit": "^1.5.0",
"@testing-library/jest-dom": "^5.11.8",
"@testing-library/react": "^11.2.2",
"@testing-library/user-event": "^12.6.0",
"axios": "^0.21.1",
"eslint": "^7.16.0",
"eslint-react": "0.0.4",
"polished": "^4.0.5",
"react": "^17.0.1",
"react-dom": "^17.0.1",
"react-dropzone": "^11.2.4",
"react-redux": "^7.2.2",
"react-router-dom": "^5.2.0",
"react-scripts": "4.0.1",
"react-transition-group": "^4.4.1",
"styled-components": "^5.2.1"
},
"scripts": {

View file

@ -1,18 +1,49 @@
import { useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { Redirect, Switch, Route, Link } from 'react-router-dom';
import { hydrate as hydrateToken } from '../../store/moderation';
import { hydrate as hydrateSamples } from '../../store/samples';
import {
Root, MainSection, ContentWrapper, WaifuSection, Waifu
} from './app.style';
import UploadForm from '../upload-form';
import LoginForm from '../login-form';
import SubmissionTracker from '../submission-tracker';
import ModerationPage from '../moderation';
function App() {
const dispatch = useDispatch();
const token = useSelector(state => state.moderation.auth.token);
useEffect(() => {
const savedToken = sessionStorage.getItem('token');
const savedSamples = sessionStorage.getItem('samples');
if (savedToken) {
dispatch(hydrateToken(savedToken));
}
if (savedSamples) {
dispatch(hydrateSamples(savedSamples));
}
}, [dispatch]);
return (
<Root>
<MainSection>
<ContentWrapper>
<h1>Hellsong offerings center</h1>
<p>
Nothing here yet. Come back later, ya?
</p>
<Switch>
<Route exact path="/" component={UploadForm} />
<Route path="/submissions/:uuid?" component={SubmissionTracker} />
<Route path="/moderation/login" component={LoginForm} />
{!token && <Redirect from="/moderation" to="/moderation/login" />}
<Route path="/moderation/queue" component={ModerationPage} />
<Route>
<h1>It's fucking nothing!</h1>
<p>Couldn't find that, gomen.</p>
<Link to="/">Head back?</Link>
</Route>
</Switch>
</ContentWrapper>
</MainSection>
<WaifuSection>

View file

@ -17,22 +17,32 @@ export const MainSection = styled.section`
`;
export const ContentWrapper = styled.div`
position: sticky;
top: 0;
max-width: 600px;
min-width: 320px;
padding: 12px;
`;
export const WaifuSection = styled.section`
flex: 1;
display: flex;
align-items: flex-end;
justify-content: flex-end;
@media (max-width: 900px) {
display: none;
}
`;
export const Waifu = styled.img.attrs({
src: tohru,
})`
z-index: -1;
position: fixed;
bottom: 0;
max-height: 100vh;
max-width: 100%;
height: 100vh;
@media (max-width: 1500px) {
height: calc(66vw - 300px);
}
`;

View file

@ -0,0 +1 @@
export { default } from './login-form';

View file

@ -0,0 +1,47 @@
import { useState, useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useHistory } from 'react-router-dom';
import { authorize, resetAuth } from '../../store/moderation';
import {
Wrapper, MainSection,
} from './login-form.style';
function LoginForm() {
const history = useHistory();
const dispatch = useDispatch();
const error = useSelector(state => state.moderation.auth.error);
const token = useSelector(state => state.moderation.auth.token);
const [secret, setSecret] = useState('');
useEffect(() => {
if (token) {
history.push('/moderation/queue');
}
}, [token]);
const handleSecretChange = (event) => {
if (error) {
dispatch(resetAuth());
}
setSecret(event.target.value);
};
const handleSubmit = () => {
dispatch(authorize(secret)).then(() => {
setSecret('');
});
};
return (
<Wrapper>
<h1>Now entering: hellsong front lines</h1>
<p>Hope you know the super-secret mod club password~</p>
<MainSection>
<input type="password" placeholder={error || 'secret go here'} value={secret} onChange={handleSecretChange} />
</MainSection>
<button onClick={handleSubmit}>Login</button>
</Wrapper>
);
}
export default LoginForm;

View file

@ -0,0 +1,21 @@
import styled from 'styled-components';
export const Wrapper = styled.div`
display: flex;
flex-direction: column;
`;
export const Subtitle = styled.a`
align-self: end;
font-size: 12px;
`;
export const MainSection = styled.div`
margin: 16px 0px;
display: flex;
flex-direction: column;
text-align: center;
input {
text-align: center;
}
`;

View file

@ -0,0 +1 @@
export { default } from './moderation';

View file

@ -0,0 +1,142 @@
import { useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { refresh, submit } from '../../store/moderation';
import {
Wrapper, SampleList, SampleWrapper, ButtonSection, MainSection,
Title, PlayIcon, PauseIcon,
} from './moderation.style';
function ModerationPage() {
const dispatch = useDispatch();
const [samples, setSamples] = useState({})
const uploads = useSelector(state => state.moderation.uploads);
useEffect(() => {
setSamples(oldSamples => (
uploads.reduce((accumulator, current) => {
accumulator[current.id] = {
...current,
progress: oldSamples[current.id]?.progress || 0,
playing: oldSamples[current.id]?.playing || false,
};
return accumulator;
}, {})
));
}, [uploads]);
useEffect(() => {
const timeout = setInterval(() => {
dispatch(refresh());
}, 5000);
return () => clearInterval(timeout);
}, [dispatch]);
const handleApprove = (sampleId) => {
dispatch(submit({
sampleId, approve: true,
}));
};
const handleDeny = (sampleId, blacklist) => {
const reason = prompt('Reason?');
if (reason != null) {
dispatch(submit({
sampleId, approve: false, reason, blacklist
}));
}
};
const handlePlayPause = (event, sampleId) => {
event.stopPropagation();
const element = document.getElementById(`audio-player_${sampleId}`);
if (samples[sampleId].playing) {
element.pause();
} else {
element.play();
}
setSamples(samples => ({
...samples,
[sampleId]: {
...samples[sampleId],
playing: !samples[sampleId].playing,
}
}));
};
const handleSeek = (event, sampleId) => {
if (!samples[sampleId].playing) {
return;
}
const wrapperElement = document.getElementById(`sample-wrapper_${sampleId}`);
const audioElement = document.getElementById(`audio-player_${sampleId}`);
const rect = wrapperElement.getClientRects()[0];
const relativeX = event.clientX - rect.x;
const percentage = relativeX / rect.width;
audioElement.currentTime = audioElement.duration * percentage;
};
const handleTimeUpdate = (event, sampleId) => {
const progress = event.target.currentTime / event.target.duration;
setSamples(samples => ({
...samples,
[sampleId]: {
...samples[sampleId],
progress,
}
}));
};
const handleEnd = (sampleId) => {
setSamples(samples => ({
...samples,
[sampleId]: {
...samples[sampleId],
playing: false,
}
}));
};
return (
<Wrapper>
<h1>Modqueue</h1>
<p>Thank's a lot for helping out! :3</p>
<SampleList>
{Object.entries(samples)?.map(([ sampleId, sample ]) => (
<SampleWrapper
key={sampleId}
id={`sample-wrapper_${sampleId}`}
>
<audio
id={`audio-player_${sampleId}`}
preload="none"
src={`${process.env.REACT_APP_SAMPLE_SERVE_URL}/${sample.path}`}
onTimeUpdate={(event) => handleTimeUpdate(event, sampleId)}
onEnded={() => handleEnd(sampleId)}
/>
<MainSection
progress={sample.progress}
onClick={(event) => handleSeek(event, sampleId)}
>
<div onClick={(event) => handlePlayPause(event, sampleId)}>
{sample.playing ? <PauseIcon /> : <PlayIcon />}
</div>
<div>
<Title>{sample.name || '(NoName McGee)'}</Title>
<span>{sample.uploader}</span>
</div>
</MainSection>
<ButtonSection>
<button onClick={() => handleApprove(sampleId)}>Approve</button>
<button onClick={() => handleDeny(sampleId, false)} className="bad">Deny</button>
<button onClick={() => handleDeny(sampleId, true)} className="bigbad">Blacklist</button>
</ButtonSection>
</SampleWrapper>
))}
</SampleList>
</Wrapper>
);
}
export default ModerationPage;

View file

@ -0,0 +1,123 @@
import styled from 'styled-components';
import { shade, tint } from 'polished';
import { ReactComponent as materialPlayIcon } from './play_arrow-white-18dp.svg';
import { ReactComponent as materialPauseIcon } from './pause-white-18dp.svg';
import { palette } from '../../index.style';
export const Wrapper = styled.div`
display: flex;
flex-direction: column;
min-width: 400px;
`;
export const SampleWrapper = styled.div`
display: flex;
flex-direction: column;
overflow: clip;
border-radius: 8px;
background-color: ${palette.base.shade1};
`;
export const SampleList = styled.div`
display: flex;
flex-direction: column;
margin-top: 16px;
${SampleWrapper}:not(:last-child) {
margin-bottom: 16px;
}
`;
export const MainSection = styled.div`
display: flex;
flex-direction: row;
padding: 12px;
user-select: none;
position: relative;
overflow: clip;
background-color: ${palette.base.shade1};
&:before {
opacity: ${props => props.progress === 1 || props.progress === 0 ? 0 : 1};
position: absolute;
top: 0;
bottom: 0;
right: 0;
left: 0;
content: '';
z-index: 0;
transform: translateX(-${props => (1 - props.progress) * 100}%);
background-color: ${tint(0.3, palette.secondary.main)};
transition: transform 0.2s, opacity 1.5s;
}
* {
z-index: 1;
}
div:nth-child(1) {
margin-right: 8px;
}
div:nth-child(2) {
display: flex;
flex-direction: column;
}
`;
export const ButtonSection = styled.div`
display: flex;
flex-direction: row;
button {
cursor: pointer;
text-size: 16px;
min-width: unset;
border-radius: 0px;
flex: 1;
background-color: ${palette.secondary.main};
&:active {
background-color: ${shade(0.2, palette.secondary.main)};
}
&.bad {
background-color: ${palette.accent.shade1};
&:active {
background-color: ${shade(0.2, palette.accent.shade1)};
}
}
&.bigbad {
background-color: ${palette.accent.main};
&:active {
background-color: ${shade(0.2, palette.accent.main)};
}
}
}
`;
export const Title = styled.span`
font-size: 18px;
color: ${palette.primary.shade1};
`;
export const PlayIcon = styled(materialPlayIcon)`
cursor: pointer;
fill: ${palette.primary.shade1};
width: 32px;
height: 32px;
`;
export const PauseIcon = styled(materialPauseIcon)`
cursor: pointer;
fill: ${palette.primary.shade1};
width: 32px;
height: 32px;
`;

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="white" width="18px" height="18px"><path d="M0 0h24v24H0z" fill="none"/><path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/></svg>

After

(image error) Size: 186 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="white" width="18px" height="18px"><path d="M0 0h24v24H0z" fill="none"/><path d="M8 5v14l11-7z"/></svg>

After

(image error) Size: 168 B

View file

@ -0,0 +1 @@
export { default } from './submission-tracker';

View file

@ -0,0 +1,64 @@
import { useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { Link } from 'react-router-dom';
import {
Wrapper, SampleWrapper, SampleList,
Title,
} from './submission-tracker.style';
import { STATUSES, poll } from '../../store/samples';
const textForStatus = {
[STATUSES.SENDING]: 'Sending to server...',
[STATUSES.REMOTE_QUEUED]: 'Queued for download...',
[STATUSES.REMOTE_DOWNLOAD]: 'Downloading...',
[STATUSES.REMOTE_CONVERT]: 'Converting...',
[STATUSES.FAILED]: 'Failed :c',
[STATUSES.REVIEW_PENDING]: 'Awaiting review...',
[STATUSES.REVIEW_REJECT]: 'Rejected by moderators!! y:',
[STATUSES.REVIEW_APPROVE]: 'Approved~',
};
const highPollRateStatuses = [STATUSES.REMOTE_QUEUED, STATUSES.REMOTE_DOWNLOAD, STATUSES.REMOTE_CONVERT];
function SubmissionTracker() {
const [pollRate, setPollRate] = useState(10000);
const dispatch = useDispatch();
const submissions = useSelector((state) => state.samples);
useEffect(() => {
if (submissions.some(({ status }) => highPollRateStatuses.includes(status))) {
setPollRate(1200);
} else {
setPollRate(10000);
}
}, [submissions])
useEffect(() => {
const timeout = setInterval(() => {
dispatch(poll());
}, pollRate);
return () => clearInterval(timeout);
}, [dispatch, pollRate]);
return (
<Wrapper>
<h1>Your offerings</h1>
<p>
Thank you kindly! This will come my way after a short trip through the mod queue.
You're free to <Link to="/">submit another</Link>, uploads will continue in the background
</p>
<SampleList>
{submissions?.map((sample) => (
<SampleWrapper key={sample.requestId} progress={sample.progress}>
<Title>{sample.name || '(NoName McGee)'}</Title>
<span>{textForStatus[sample.status]}</span>
{sample.status === STATUSES.REVIEW_REJECT && <span>Reason: <i>{sample.rejectMessage}</i></span>}
</SampleWrapper>
))}
</SampleList>
</Wrapper>
);
}
export default SubmissionTracker;

View file

@ -0,0 +1,55 @@
import styled from 'styled-components';
import { tint } from 'polished';
import { palette } from '../../index.style';
export const Wrapper = styled.div`
display: flex;
flex-direction: column;
`;
export const SampleWrapper = styled.div`
display: flex;
flex-direction: column;
padding: 12px;
border-radius: 8px;
position: relative;
overflow: clip;
background-color: ${palette.base.shade1};
&:before {
opacity: ${props => props.progress === 1 ? 0 : 1};
position: absolute;
top: 0;
bottom: 0;
right: 0;
left: 0;
content: '';
z-index: 0;
transform: translateX(-${props => (1 - props.progress) * 100}%);
background-color: ${tint(0.3, palette.secondary.main)};
transition: transform 0.3s, opacity 1.5s;
}
* {
z-index: 1;
}
`;
export const SampleList = styled.div`
display: flex;
flex-direction: column;
margin-top: 16px;
${SampleWrapper}:not(:last-child) {
margin-bottom: 16px;
}
`;
export const Title = styled.span`
font-size: 18px;
color: ${palette.primary.shade1};
`;

View file

@ -0,0 +1,2 @@
export { default } from './transition';
export { scrollTransition } from './transition.style';

View file

@ -0,0 +1,36 @@
import { useRef, useState, useEffect } from 'react';
import { CSSTransition } from 'react-transition-group';
import { DynamicStyledDiv } from './transition.style';
function Transition(props) {
const { children, transition, in: inProp, ...rest } = props;
const [height, setHeight] = useState();
const [internalInProp, setInternalInProp] = useState(false);
const divRef = useRef();
useEffect(() => {
setHeight(divRef.current.offsetHeight);
setInternalInProp(inProp);
}, [inProp]);
return (
<CSSTransition
classNames="transition"
addEndListener={(node, done) => {
node.addEventListener('transitionend', (e) => {
if (e.target === node) {
done(e);
};
}, false);
}}
in={internalInProp}
{...rest}
>
<DynamicStyledDiv ref={divRef} transition={transition} height={height}>
{children}
</DynamicStyledDiv>
</CSSTransition>
);
}
export default Transition;

View file

@ -0,0 +1,30 @@
import styled, { css } from 'styled-components';
export const DynamicStyledDiv = styled.div`
${props => props.transition};
`;
export const scrollTransition = css`
display: flex;
flex-direction: column;
&.transition-enter {
opacity: 0;
margin-top: -${(props) => props.height}px;
}
&.transition-enter-active {
opacity: 1;
margin-top: 0;
transition: opacity 0.3s, margin-top 0.3s;
}
&.transition-exit {
opacity: 1;
margin-top: 0;
}
&.transition-exit-active {
opacity: 0;
margin-top: -${(props) => props.height}px;
transition: opacity 0.3s, margin-top 0.3s;
}
`;

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="white" width="18px" height="18px"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M19.35 10.04C18.67 6.59 15.64 4 12 4 9.11 4 6.6 5.64 5.35 8.04 2.34 8.36 0 10.91 0 14c0 3.31 2.69 6 6 6h13c2.76 0 5-2.24 5-5 0-2.64-2.05-4.78-4.65-4.96zM14 13v4h-4v-4H7l5-5 5 5h-3z"/></svg>

After

(image error) Size: 337 B

View file

@ -0,0 +1,27 @@
import {
Dropzone, Title, UploadIcon, DeleteIcon,
} from './file-dropzone.style';
import { useDropzone } from 'react-dropzone';
function FileDropzone({
title, onDrop, filename
}) {
const { getRootProps, getInputProps, isDragActive } = useDropzone({ onDropAccepted: onDrop, accept: 'audio/*', maxFiles: 1 });
const handleDelete = (event) => {
event.stopPropagation();
onDrop(null);
};
return (
<Dropzone
{...getRootProps()}
className={isDragActive && 'dragactive'}
>
<input {...getInputProps()} />
<Title>{filename || title}</Title>
{filename ? <DeleteIcon onClick={handleDelete} /> : <UploadIcon />}
</Dropzone>
);
}
export default FileDropzone;

View file

@ -0,0 +1,49 @@
import styled, { css } from 'styled-components';
import { palette } from '../../../index.style';
import { ReactComponent as materialUploadSvg } from './cloud_upload-white-18dp.svg';
import { ReactComponent as materialRemoveSvg } from './remove_circle-white-18dp.svg';
export const Dropzone = styled.div`
padding: 8px 24px;
display: flex;
flex-direction: row;
align-items: center;
border-radius: 12px;
background-color: ${palette.base.shade1};
border: 2px dashed ${palette.accent.main};
&:hover, &.dragactive {
background-color: ${palette.base.main};
border-color: ${palette.accent.shade1};
transition: all 0.13s ease-out;
}
transition: all 0.2s ease-in;
cursor: pointer;
`;
export const Title = styled.span`
flex: 1;
text-align: center;
user-select: none;
`;
const iconStyle = css`
width: 32px;
height: 32px;
fill: ${palette.accent.main};
${Dropzone}:hover &, ${Dropzone}.dragactive & {
fill: ${palette.accent.shade1};
transition: fill 0.13s ease-out;
}
transition: fill 0.2s ease-in;
`;
export const UploadIcon = styled(materialUploadSvg)`
${iconStyle};
`;
export const DeleteIcon = styled(materialRemoveSvg)`
${iconStyle};
`;

View file

@ -0,0 +1 @@
export { default } from './file-dropzone';

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="white" width="18px" height="18px"><path d="M0 0h24v24H0z" fill="none"/><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm5 11H7v-2h10v2z"/></svg>

After

(image error) Size: 237 B

View file

@ -0,0 +1 @@
export { default } from './upload-form';

View file

@ -0,0 +1,86 @@
import { useState } from 'react';
import { useDispatch } from 'react-redux';
import { useHistory } from 'react-router-dom';
import { TransitionGroup } from 'react-transition-group';
import { submit } from '../../store/samples';
import FileDropzone from './file-dropzone';
import BaseTransition, { scrollTransition } from '../transition';
import {
Wrapper, MainSection, LegalSection, Subtitle,
} from './upload-form.style';
function UploadForm() {
const dispatch = useDispatch();
const history = useHistory();
const [file, setFile] = useState();
const [url, setUrl] = useState('');
const [submitter, setSubmitter] = useState('');
const onDrop = (files) => {
setFile(files ? files[0] : null);
};
const handleUrlChange = (event) => {
setUrl(event.target.value);
};
const handleSubmitterChange = (event) => {
setSubmitter(event.target.value);
};
const handleSubmit = () => {
history.push('/submissions');
dispatch(submit({
file, url, submitter,
}));
};
const formValid = (url || file) && submitter;
return (
<Wrapper>
<h1>Hellsong offerings center</h1>
<p>
<span>Your destination for </span>
<a href="https://youtu.be/arYAm7agoBo" target="_blank" rel="noreferrer">hellsong 2021</a>
<span> submissions~</span>
</p>
<MainSection>
<TransitionGroup>
{!url && (
<BaseTransition transition={scrollTransition}>
<FileDropzone title="Upload from your device" onDrop={onDrop} filename={file?.name} />
</BaseTransition>
)}
{!url && !file && (
<BaseTransition transition={scrollTransition}>
<p style={{margin: 0, padding: '16px 0'}}>or</p>
</BaseTransition>
)}
{!file && (
<BaseTransition transition={scrollTransition}>
<input type="url" placeholder="from elsewhere on the internet" value={url} onChange={handleUrlChange} />
<Subtitle href="http://ytdl-org.github.io/youtube-dl/supportedsites.html" target="_blank" rel="noreferrer">
<i>Any</i> site? Yeah just about - click here for a full list
</Subtitle>
</BaseTransition>
)}
</TransitionGroup>
</MainSection>
<hr />
<LegalSection>
<span>By clicking "submit" below, you agree that either:</span>
<ul>
<li>You have no rights over the provided work and are requesting it be used in a fair-use manner, or</li>
<li>
<span>You ARE authorized to sublicense the sampled work and grant the webhost (skeh@is.nota.live) a </span>
<a href="https://creativecommons.org/licenses/by/4.0/" target="_blank" rel="noreferrer">CC BY 4.0</a>
<span> license to this sample.</span>
</li>
</ul>
<input type="text" placeholder="Name / handle to attribute" value={submitter} onChange={handleSubmitterChange} />
</LegalSection>
<button onClick={handleSubmit} disabled={!formValid}>Submit!</button>
</Wrapper>
);
}
export default UploadForm;

View file

@ -0,0 +1,30 @@
import styled from 'styled-components';
import { palette } from '../../index.style';
export const Wrapper = styled.div`
display: flex;
flex-direction: column;
`;
export const Subtitle = styled.a`
align-self: end;
font-size: 12px;
`;
export const MainSection = styled.div`
margin: 16px 0px;
display: flex;
flex-direction: column;
text-align: center;
input {
text-align: center;
}
`;
export const LegalSection = styled.div`
padding: 16px 0px;
color: ${palette.primary.main};
font-style: italic;
font-size: 16px;
`;

View file

@ -1,12 +0,0 @@
import React from 'react';
import ReactDOM from 'react-dom';
import App from './components/app';
import { GlobalStyle } from './index.style';
ReactDOM.render(
<React.StrictMode>
<GlobalStyle />
<App />
</React.StrictMode>,
document.getElementById('root')
);

18
src/index.jsx Normal file
View file

@ -0,0 +1,18 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter as Router } from 'react-router-dom';
import { Provider } from 'react-redux'
import App from './components/app';
import { GlobalStyle } from './index.style';
import store from './store';
ReactDOM.render(
<Provider store={store}>
<Router>
<GlobalStyle />
<App />
</Router>
</Provider>,
document.getElementById('root')
);

View file

@ -1,4 +1,5 @@
import { createGlobalStyle } from 'styled-components';
import { normalize, desaturate } from 'polished';
export const palette = {
base: {
@ -14,21 +15,23 @@ export const palette = {
shade1: '#6155a6',
},
accent: {
main: '#b5f0c9',
shade1: '#80e8a3',
main: '#e84a5f',
shade1: '#ff847c',
}
};
export const GlobalStyle = createGlobalStyle`
${normalize()};
body {
background-color: ${palette.base.main};
display: flex;
margin: 0;
font-family: 'Lato', sans-serif;
}
p, a, span, h1, h2, h3, h4 {
font-family: 'Lato', sans-serif;
p, a, span, input, h1, h2, h3, h4 {
color: ${palette.primary.shade1};
}
h1 {
@ -45,14 +48,16 @@ export const GlobalStyle = createGlobalStyle`
h4 {
font-size: 20px;
}
p, a, span {
p, a, span, input {
font-size: 16px;
color: ${palette.primary.main};
}
code {
font-family: 'Anonymous Pro', monospace;
font-size: 16px;
}
a {
color: ${palette.secondary.shade1};
&:visited {
@ -60,6 +65,51 @@ export const GlobalStyle = createGlobalStyle`
}
}
hr {
width: 100%;
opacity: 0.3;
color: ${palette.secondary.main};
}
input {
border: 0;
outline: none;
padding: 4px 12px;
background-color: ${palette.base.shade1};
color: ${palette.primary.shade1};
&:focus {
outline: solid 2px ${palette.secondary.main};
}
}
button {
font-size: 16px;
border: 0;
border-radius 12px;
outline: none;
padding: 8px 24px;
background-color: ${palette.accent.shade1};
color: black;
&:active {
background-color: ${palette.accent.main};
transition: unset;
}
&[disabled] {
background-color: ${desaturate(0.9, palette.accent.shade1)};
}
transition: background-color 0.23s ease-out;
min-width: 77%;
max-width: 80%;
align-self: center;
}
#root {
width: 100vw;
height: 100vh;

29
src/store/index.js Normal file
View file

@ -0,0 +1,29 @@
import { configureStore } from '@reduxjs/toolkit';
import moderation from './moderation';
import samples from './samples';
const store = configureStore({
reducer: {
moderation,
samples,
},
});
let currentToken = null;
let currentSamples = JSON.stringify([]);
store.subscribe(() => {
const previousToken = currentToken;
const previousSamples = currentSamples;
const { moderation: { auth: { token } }, samples } = store.getState();
currentToken = token;
currentSamples = JSON.stringify(samples);
if (previousToken !== currentToken) {
sessionStorage.setItem('token', currentToken);
}
if (previousSamples !== currentSamples) {
sessionStorage.setItem('samples', currentSamples);
}
});
export default store;

94
src/store/moderation.js Normal file
View file

@ -0,0 +1,94 @@
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import axios from 'axios';
export const authorize = createAsyncThunk(
'moderation/authorize',
async (secret, { rejectWithValue }) => {
try {
const result = await axios.post(process.env.REACT_APP_MOD_AUTH_ENDPOINT, { secret });
return result.data;
} catch (e) {
return rejectWithValue(e.response?.data?.description ?? e.message)
}
},
);
export const refresh = createAsyncThunk(
'moderation/refresh',
async (_, { getState, rejectWithValue }) => {
try {
const token = getState().moderation.auth.token;
const result = await axios.get(process.env.REACT_APP_MOD_ENDPOINT, {
headers: {'Authorization': `JWT ${token}`},
});
return result.data.samples;
} catch (e) {
return rejectWithValue(e.response?.data?.message ?? e.message);
}
}
);
export const submit = createAsyncThunk(
'moderation/submit',
async ({ sampleId, approve, reason, blacklist }, { getState, rejectWithValue }) => {
try {
const token = getState().moderation.auth.token;
const result = await axios.post(`${process.env.REACT_APP_MOD_ENDPOINT}/${sampleId}`, {
approve, reason, blacklist,
}, { headers: {'Authorization': `JWT ${token}`} });
return result.data;
} catch (e) {
return rejectWithValue(e.response?.data?.message ?? e.message);
}
}
);
const initialState = {
uploads: [],
auth: {
token: null,
error: null,
inFlight: false,
},
};
const samplesSlice = createSlice({
name: 'moderation',
initialState,
reducers: {
hydrate(state, { payload }) {
state.auth.token = payload;
},
resetAuth(state) {
state.auth = initialState.auth;
},
reset(state) {
return initialState;
},
},
extraReducers: {
[authorize.pending]: (state) => {
state.auth.inFlight = true;
},
[authorize.rejected]: (state, { payload }) => {
state.auth.error = payload;
state.auth.inFlight = false;
},
[authorize.fulfilled]: (state, { payload }) => {
state.auth.token = payload.access_token;
state.auth.inFlight = false;
},
[refresh.fulfilled]: (state, { payload }) => {
state.uploads = payload;
},
[submit.fulfilled]: (state, { meta: { arg } }) => {
const index = state.uploads.findIndex((entity) => entity.id === arg.sampleId);
state.uploads.splice(index, 1);
}
}
});
export default samplesSlice.reducer;
export const { reset, resetAuth, hydrate } = samplesSlice.actions;

154
src/store/samples.js Normal file
View file

@ -0,0 +1,154 @@
import { createSlice, createAction, createAsyncThunk } from '@reduxjs/toolkit';
import axios from 'axios';
export const STATUSES = {
SENDING: 0,
REMOTE_QUEUED: 1,
REMOTE_DOWNLOAD: 2,
REMOTE_CONVERT: 3,
FAILED: 4,
REVIEW_PENDING: 5,
REVIEW_REJECT: 6,
REVIEW_APPROVE: 7,
};
const eolStatuses = [STATUSES.FAILED, STATUSES.REVIEW_APPROVE, STATUSES.REVIEW_REJECT];
export const setProgress = createAction('samples/setProgress');
export const submit = createAsyncThunk(
'samples/submit',
async ({ file, url, submitter }, { dispatch, getState, requestId, rejectWithValue }) => {
try {
const index = getState().samples.findIndex((entity) => entity.requestId === requestId);
if (url) {
const result = await axios.post(process.env.REACT_APP_SUBMIT_URL_ENDPOINT, {
url,
submitter,
});
return { id: result.data.id, transient: true };
}
let formData = new FormData();
formData.append('file', file);
formData.append('submitter', submitter);
const onUploadProgress = (e) => {
const progress = e.loaded / e.total;
dispatch(setProgress({ progress, index }));
}
const result = await axios.post(process.env.REACT_APP_SUBMIT_FILE_ENDPOINT, formData, {
headers: { 'Content-Type': 'multipart/form-data' },
onUploadProgress,
});
return { id: result.data.id, transient: false };
} catch (e) {
return rejectWithValue({
message: e.response?.data?.message ?? e.message,
serverFault: e.response?.code >= 500,
});
}
},
);
export const poll = createAsyncThunk(
'samples/poll',
async (_, { getState, rejectWithValue }) => {
try {
const sampleIds = getState().samples
.filter(sample => sample.id && !eolStatuses.includes(sample.status))
.map((sample) => sample.id);
const result = await axios.post(process.env.REACT_APP_STATUS_ENDPOINT, { samples: sampleIds });
return result.data.data;
} catch (e) {
return rejectWithValue(e.response?.data?.message ?? e.message);
}
}
)
const initialState = [];
const samplesSlice = createSlice({
name: 'samples',
initialState,
reducers: {
hydrate(state, { payload }) {
const newState = JSON.parse(payload);
return newState.filter((upload) => upload.status !== STATUSES.SENDING && !eolStatuses.includes(upload.status));
},
reset(state) {
return initialState;
},
},
extraReducers: {
[submit.pending]: (state, { meta: { arg, requestId } }) => {
state.push({
id: null,
requestId,
inFlight: true,
canRetry: false,
progress: 0,
name: arg?.file?.name || arg?.file?.path || arg?.url,
submitter: arg?.submitter,
status: STATUSES.SENDING,
});
},
[setProgress]: (state, { payload }) => {
state[payload.index].progress = payload.progress;
},
[submit.rejected]: (state, { payload, meta: { requestId } }) => {
const index = state.findIndex((entity) => entity.requestId === requestId);
if (index !== -1) {
state[index].inFlight = false;
state[index].status = payload.message;
state[index].canRetry = payload.serverFault;
}
},
[submit.fulfilled]: (state, { payload, meta: { requestId } }) => {
const index = state.findIndex((entity) => entity.requestId === requestId);
state[index].inFlight = false;
state[index].id = payload.id;
if (!payload.transient) {
state[index].progress = 1;
state[index].status = STATUSES.REVIEW_PENDING;
} else {
state[index].status = STATUSES.REMOTE_QUEUED;
}
},
[poll.fulfilled]: (state, { payload }) => {
payload.forEach(update => {
const index = state.findIndex((entity) => entity.id === update.uuid);
if (update.transient) {
if (update.progress < 0) {
state[index].status = STATUSES.REMOTE_QUEUED;
} else if (update.progress < 1) {
state[index].status = STATUSES.REMOTE_DOWNLOAD;
state[index].progress = update.progress * 0.75;
} else {
state[index].status = STATUSES.REMOTE_CONVERT;
}
} else {
state[index].progress = 1;
state[index].name = update.title;
if (update.approved) {
state[index].status = STATUSES.REVIEW_APPROVE;
} else if (update.rejectionMessage) {
if (update.rejectionMessage === 'Failed to download') {
state[index].status = STATUSES.FAILED;
} else {
state[index].status = STATUSES.REVIEW_REJECT;
state[index].rejectMessage = update.rejectionMessage;
}
} else {
state[index].status = STATUSES.REVIEW_PENDING;
}
}
});
}
}
});
export default samplesSlice.reducer;
export const { reset, hydrate } = samplesSlice.actions;