ALL THE THIGNS KLSJDFLKDJSFLSDF
This commit is contained in:
parent
8760f61f65
commit
72691532e7
32 changed files with 1331 additions and 25 deletions
package-lock.jsonpackage.jsonindex.jsindex.jsxindex.style.js
src
components
app
login-form
moderation
submission-tracker
transition
upload-form
store
205
package-lock.json
generated
205
package-lock.json
generated
|
@ -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",
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
`;
|
||||
|
|
1
src/components/login-form/index.js
Normal file
1
src/components/login-form/index.js
Normal file
|
@ -0,0 +1 @@
|
|||
export { default } from './login-form';
|
47
src/components/login-form/login-form.jsx
Normal file
47
src/components/login-form/login-form.jsx
Normal 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;
|
21
src/components/login-form/login-form.style.js
Normal file
21
src/components/login-form/login-form.style.js
Normal 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;
|
||||
}
|
||||
`;
|
1
src/components/moderation/index.js
Normal file
1
src/components/moderation/index.js
Normal file
|
@ -0,0 +1 @@
|
|||
export { default } from './moderation';
|
142
src/components/moderation/moderation.jsx
Normal file
142
src/components/moderation/moderation.jsx
Normal 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;
|
123
src/components/moderation/moderation.style.js
Normal file
123
src/components/moderation/moderation.style.js
Normal 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;
|
||||
`;
|
1
src/components/moderation/pause-white-18dp.svg
Normal file
1
src/components/moderation/pause-white-18dp.svg
Normal 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 |
1
src/components/moderation/play_arrow-white-18dp.svg
Normal file
1
src/components/moderation/play_arrow-white-18dp.svg
Normal 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 |
1
src/components/submission-tracker/index.js
Normal file
1
src/components/submission-tracker/index.js
Normal file
|
@ -0,0 +1 @@
|
|||
export { default } from './submission-tracker';
|
64
src/components/submission-tracker/submission-tracker.jsx
Normal file
64
src/components/submission-tracker/submission-tracker.jsx
Normal 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;
|
|
@ -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};
|
||||
|
||||
`;
|
2
src/components/transition/index.js
Normal file
2
src/components/transition/index.js
Normal file
|
@ -0,0 +1,2 @@
|
|||
export { default } from './transition';
|
||||
export { scrollTransition } from './transition.style';
|
36
src/components/transition/transition.jsx
Normal file
36
src/components/transition/transition.jsx
Normal 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;
|
30
src/components/transition/transition.style.js
Normal file
30
src/components/transition/transition.style.js
Normal 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;
|
||||
}
|
||||
`;
|
|
@ -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 |
27
src/components/upload-form/file-dropzone/file-dropzone.jsx
Normal file
27
src/components/upload-form/file-dropzone/file-dropzone.jsx
Normal 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;
|
|
@ -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};
|
||||
`;
|
1
src/components/upload-form/file-dropzone/index.js
Normal file
1
src/components/upload-form/file-dropzone/index.js
Normal file
|
@ -0,0 +1 @@
|
|||
export { default } from './file-dropzone';
|
|
@ -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 |
1
src/components/upload-form/index.js
Normal file
1
src/components/upload-form/index.js
Normal file
|
@ -0,0 +1 @@
|
|||
export { default } from './upload-form';
|
86
src/components/upload-form/upload-form.jsx
Normal file
86
src/components/upload-form/upload-form.jsx
Normal 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;
|
30
src/components/upload-form/upload-form.style.js
Normal file
30
src/components/upload-form/upload-form.style.js
Normal 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;
|
||||
`;
|
12
src/index.js
12
src/index.js
|
@ -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
18
src/index.jsx
Normal 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')
|
||||
);
|
|
@ -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
29
src/store/index.js
Normal 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
94
src/store/moderation.js
Normal 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
154
src/store/samples.js
Normal 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;
|
Loading…
Add table
Reference in a new issue