Material Redesign #1
@ -114,7 +114,7 @@ def auth():
|
||||
'client_id': client_id,
|
||||
'response_type': 'code',
|
||||
'scope': 'playlist-modify-public playlist-modify-private playlist-read-private user-read-playback-state user-modify-playback-state user-library-read',
|
||||
'redirect_uri': 'https://spotify.sarsoo.xyz/auth/spotify/token'
|
||||
'redirect_uri': 'https://music.sarsoo.xyz/auth/spotify/token'
|
||||
}
|
||||
)
|
||||
|
||||
@ -141,7 +141,7 @@ def token():
|
||||
data = {
|
||||
'grant_type': 'authorization_code',
|
||||
'code': code,
|
||||
'redirect_uri': 'https://spotify.sarsoo.xyz/auth/spotify/token'
|
||||
'redirect_uri': 'https://music.sarsoo.xyz/auth/spotify/token'
|
||||
}
|
||||
|
||||
req = requests.post('https://accounts.spotify.com/api/token', data=data, headers=headers)
|
||||
|
@ -5,6 +5,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link type="text/css" rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
|
||||
<link href="https://fonts.googleapis.com/css?family=Pirata+One|Roboto" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" />
|
||||
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="https://storage.googleapis.com/sarsooxyzstatic/apple-touch-icon.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="https://storage.googleapis.com/sarsooxyzstatic/favicon-32x32.png">
|
||||
|
245
package-lock.json
generated
245
package-lock.json
generated
@ -997,6 +997,88 @@
|
||||
"to-fast-properties": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"@emotion/hash": {
|
||||
"version": "0.7.4",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.7.4.tgz",
|
||||
"integrity": "sha512-fxfMSBMX3tlIbKUdtGKxqB1fyrH6gVrX39Gsv3y8lRYKUqlgDt3UMqQyGnR1bQMa2B8aGnhLZokZgg8vT0Le+A=="
|
||||
},
|
||||
"@material-ui/core": {
|
||||
"version": "4.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@material-ui/core/-/core-4.9.0.tgz",
|
||||
"integrity": "sha512-zrrr8mPU5DDBYaVil4uJYauW41PjSn5otn7cqGsmWOY0t90fypr9nNgM7rRJaPz2AP6oRSDx1kBQt2igf5uelg==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.4.4",
|
||||
"@material-ui/styles": "^4.9.0",
|
||||
"@material-ui/system": "^4.7.1",
|
||||
"@material-ui/types": "^5.0.0",
|
||||
"@material-ui/utils": "^4.7.1",
|
||||
"@types/react-transition-group": "^4.2.0",
|
||||
"clsx": "^1.0.2",
|
||||
"convert-css-length": "^2.0.1",
|
||||
"hoist-non-react-statics": "^3.2.1",
|
||||
"normalize-scroll-left": "^0.2.0",
|
||||
"popper.js": "^1.14.1",
|
||||
"prop-types": "^15.7.2",
|
||||
"react-is": "^16.8.0",
|
||||
"react-transition-group": "^4.3.0"
|
||||
}
|
||||
},
|
||||
"@material-ui/icons": {
|
||||
"version": "4.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@material-ui/icons/-/icons-4.5.1.tgz",
|
||||
"integrity": "sha512-YZ/BgJbXX4a0gOuKWb30mBaHaoXRqPanlePam83JQPZ/y4kl+3aW0Wv9tlR70hB5EGAkEJGW5m4ktJwMgxQAeA==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.4.4"
|
||||
}
|
||||
},
|
||||
"@material-ui/styles": {
|
||||
"version": "4.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@material-ui/styles/-/styles-4.9.0.tgz",
|
||||
"integrity": "sha512-nJHum4RqYBPWsjL/9JET8Z02FZ9gSizlg/7LWVFpIthNzpK6OQ5OSRR4T4x9/p+wK3t1qNn3b1uI4XpnZaPxOA==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.4.4",
|
||||
"@emotion/hash": "^0.7.4",
|
||||
"@material-ui/types": "^5.0.0",
|
||||
"@material-ui/utils": "^4.7.1",
|
||||
"clsx": "^1.0.2",
|
||||
"csstype": "^2.5.2",
|
||||
"hoist-non-react-statics": "^3.2.1",
|
||||
"jss": "^10.0.3",
|
||||
"jss-plugin-camel-case": "^10.0.3",
|
||||
"jss-plugin-default-unit": "^10.0.3",
|
||||
"jss-plugin-global": "^10.0.3",
|
||||
"jss-plugin-nested": "^10.0.3",
|
||||
"jss-plugin-props-sort": "^10.0.3",
|
||||
"jss-plugin-rule-value-function": "^10.0.3",
|
||||
"jss-plugin-vendor-prefixer": "^10.0.3",
|
||||
"prop-types": "^15.7.2"
|
||||
}
|
||||
},
|
||||
"@material-ui/system": {
|
||||
"version": "4.7.1",
|
||||
"resolved": "https://registry.npmjs.org/@material-ui/system/-/system-4.7.1.tgz",
|
||||
"integrity": "sha512-zH02p+FOimXLSKOW/OT2laYkl9bB3dD1AvnZqsHYoseUaq0aVrpbl2BGjQi+vJ5lg8w73uYlt9zOWzb3+1UdMQ==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.4.4",
|
||||
"@material-ui/utils": "^4.7.1",
|
||||
"prop-types": "^15.7.2"
|
||||
}
|
||||
},
|
||||
"@material-ui/types": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@material-ui/types/-/types-5.0.0.tgz",
|
||||
"integrity": "sha512-UeH2BuKkwDndtMSS0qgx1kCzSMw+ydtj0xx/XbFtxNSTlXydKwzs5gVW5ZKsFlAkwoOOQ9TIsyoCC8hq18tOwg=="
|
||||
},
|
||||
"@material-ui/utils": {
|
||||
"version": "4.7.1",
|
||||
"resolved": "https://registry.npmjs.org/@material-ui/utils/-/utils-4.7.1.tgz",
|
||||
"integrity": "sha512-+ux0SlLdlehvzCk2zdQ3KiS3/ylWvuo/JwAGhvb8dFVvwR21K28z0PU9OQW2PGogrMEdvX3miEI5tGxTwwWiwQ==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.4.4",
|
||||
"prop-types": "^15.7.2",
|
||||
"react-is": "^16.8.0"
|
||||
}
|
||||
},
|
||||
"@types/anymatch": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/anymatch/-/anymatch-1.3.1.tgz",
|
||||
@ -1032,6 +1114,28 @@
|
||||
"integrity": "sha512-aX+gFgA5GHcDi89KG5keey2zf0WfZk/HAQotEamsK2kbey+8yGKcson0hbK8E+v0NArlCJQCqMP161YhV6ZXLg==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/prop-types": {
|
||||
"version": "15.7.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.3.tgz",
|
||||
"integrity": "sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw=="
|
||||
},
|
||||
"@types/react": {
|
||||
"version": "16.9.19",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-16.9.19.tgz",
|
||||
"integrity": "sha512-LJV97//H+zqKWMms0kvxaKYJDG05U2TtQB3chRLF8MPNs+MQh/H1aGlyDUxjaHvu08EAGerdX2z4LTBc7ns77A==",
|
||||
"requires": {
|
||||
"@types/prop-types": "*",
|
||||
"csstype": "^2.2.0"
|
||||
}
|
||||
},
|
||||
"@types/react-transition-group": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.2.3.tgz",
|
||||
"integrity": "sha512-Hk8jiuT7iLOHrcjKP/ZVSyCNXK73wJAUz60xm0mVhiRujrdiI++j4duLiL282VGxwAgxetHQFfqA29LgEeSkFA==",
|
||||
"requires": {
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"@types/tapable": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/tapable/-/tapable-1.0.4.tgz",
|
||||
@ -1895,6 +1999,11 @@
|
||||
"wrap-ansi": "^5.1.0"
|
||||
}
|
||||
},
|
||||
"clsx": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-1.0.4.tgz",
|
||||
"integrity": "sha512-1mQ557MIZTrL/140j+JVdRM6e31/OA4vTYxXgqIIZlndyfjHpyawKZia1Im05Vp9BWmImkcNrNtFYQMyFcgJDg=="
|
||||
},
|
||||
"collection-visit": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz",
|
||||
@ -1966,6 +2075,11 @@
|
||||
"integrity": "sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U=",
|
||||
"dev": true
|
||||
},
|
||||
"convert-css-length": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/convert-css-length/-/convert-css-length-2.0.1.tgz",
|
||||
"integrity": "sha512-iGpbcvhLPRKUbBc0Quxx7w/bV14AC3ItuBEGMahA5WTYqB8lq9jH0kTXFheCBASsYnqeMFZhiTruNxr1N59Axg=="
|
||||
},
|
||||
"convert-source-map": {
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz",
|
||||
@ -2108,12 +2222,26 @@
|
||||
"schema-utils": "^2.6.0"
|
||||
}
|
||||
},
|
||||
"css-vendor": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/css-vendor/-/css-vendor-2.0.7.tgz",
|
||||
"integrity": "sha512-VS9Rjt79+p7M0WkPqcAza4Yq1ZHrsHrwf7hPL/bjQB+c1lwmAI+1FXxYTYt818D/50fFVflw0XKleiBN5RITkg==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.6.2",
|
||||
"is-in-browser": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"cssesc": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
|
||||
"integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
|
||||
"dev": true
|
||||
},
|
||||
"csstype": {
|
||||
"version": "2.6.8",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.8.tgz",
|
||||
"integrity": "sha512-msVS9qTuMT5zwAGCVm4mxfrZ18BNc6Csd0oJAtiFMZ1FAx1CCvy2+5MDmYoix63LM/6NDbNtodCiGYGmFgO0dA=="
|
||||
},
|
||||
"cyclist": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/cyclist/-/cyclist-1.0.1.tgz",
|
||||
@ -2233,6 +2361,15 @@
|
||||
"randombytes": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"dom-helpers": {
|
||||
"version": "5.1.3",
|
||||
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.1.3.tgz",
|
||||
"integrity": "sha512-nZD1OtwfWGRBWlpANxacBEZrEuLa16o1nh7YopFWeoF68Zt8GGEmzHu6Xv4F3XaFIC+YXtTLrzgqKxFgLEe4jw==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.6.3",
|
||||
"csstype": "^2.6.7"
|
||||
}
|
||||
},
|
||||
"domain-browser": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz",
|
||||
@ -3490,6 +3627,11 @@
|
||||
"integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=",
|
||||
"dev": true
|
||||
},
|
||||
"hyphenate-style-name": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.0.3.tgz",
|
||||
"integrity": "sha512-EcuixamT82oplpoJ2XU4pDtKGWQ7b00CD9f1ug9IaQ3p1bkHMiKCZ9ut9QDI6qsa6cpUuB+A/I+zLtdNK4n2DQ=="
|
||||
},
|
||||
"icss-utils": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-4.1.1.tgz",
|
||||
@ -3683,6 +3825,11 @@
|
||||
"is-extglob": "^2.1.1"
|
||||
}
|
||||
},
|
||||
"is-in-browser": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/is-in-browser/-/is-in-browser-1.1.3.tgz",
|
||||
"integrity": "sha1-Vv9NtoOgeMYILrldrX3GLh0E+DU="
|
||||
},
|
||||
"is-number": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz",
|
||||
@ -3812,6 +3959,83 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"jss": {
|
||||
"version": "10.0.3",
|
||||
"resolved": "https://registry.npmjs.org/jss/-/jss-10.0.3.tgz",
|
||||
"integrity": "sha512-AcDvFdOk16If9qvC9KN3oFXsrkHWM9+TaPMpVB9orm3z+nq1Xw3ofHyflRe/mkSucRZnaQtlhZs1hdP3DR9uRw==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.3.1",
|
||||
"csstype": "^2.6.5",
|
||||
"is-in-browser": "^1.1.3",
|
||||
"tiny-warning": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"jss-plugin-camel-case": {
|
||||
"version": "10.0.3",
|
||||
"resolved": "https://registry.npmjs.org/jss-plugin-camel-case/-/jss-plugin-camel-case-10.0.3.tgz",
|
||||
"integrity": "sha512-rild/oFKFkmRP7AoiX9D6bdDAUfmJv8c7sEBvFoi+JP31dn2W8nw4txMKGnV1LJKlFkYprdZt1X99Uvztl1hug==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.3.1",
|
||||
"hyphenate-style-name": "^1.0.3",
|
||||
"jss": "^10.0.3"
|
||||
}
|
||||
},
|
||||
"jss-plugin-default-unit": {
|
||||
"version": "10.0.3",
|
||||
"resolved": "https://registry.npmjs.org/jss-plugin-default-unit/-/jss-plugin-default-unit-10.0.3.tgz",
|
||||
"integrity": "sha512-n+XfVLPF9Qh7IOTdQ8M4oRpjpg6egjr/r0NNytubbCafMgCILJYIVrMTGgOTydH+uvak8onQY3f/F9hasPUx6g==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.3.1",
|
||||
"jss": "^10.0.3"
|
||||
}
|
||||
},
|
||||
"jss-plugin-global": {
|
||||
"version": "10.0.3",
|
||||
"resolved": "https://registry.npmjs.org/jss-plugin-global/-/jss-plugin-global-10.0.3.tgz",
|
||||
"integrity": "sha512-kNotkAciJIXpIGYnmueaIifBne9rdq31O8Xq1nF7KMfKlskNRANTcEX5rVnsGKl2yubTMYfjKBFCeDgcQn6+gA==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.3.1",
|
||||
"jss": "^10.0.3"
|
||||
}
|
||||
},
|
||||
"jss-plugin-nested": {
|
||||
"version": "10.0.3",
|
||||
"resolved": "https://registry.npmjs.org/jss-plugin-nested/-/jss-plugin-nested-10.0.3.tgz",
|
||||
"integrity": "sha512-OMucRs9YLvWlZ3Ew+VhdgNVMwSS2zZy/2vy+s/etvopnPUzDHgCnJwdY2Wx/SlhLGERJeKKufyih2seH+ui0iw==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.3.1",
|
||||
"jss": "^10.0.3",
|
||||
"tiny-warning": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"jss-plugin-props-sort": {
|
||||
"version": "10.0.3",
|
||||
"resolved": "https://registry.npmjs.org/jss-plugin-props-sort/-/jss-plugin-props-sort-10.0.3.tgz",
|
||||
"integrity": "sha512-ufhvdCMnRcDa0tNHoZ12OcVNQQyE10yLMohxo/UIMarLV245rM6n9D19A12epjldRgyiS13SoSyLFCJEobprYg==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.3.1",
|
||||
"jss": "^10.0.3"
|
||||
}
|
||||
},
|
||||
"jss-plugin-rule-value-function": {
|
||||
"version": "10.0.3",
|
||||
"resolved": "https://registry.npmjs.org/jss-plugin-rule-value-function/-/jss-plugin-rule-value-function-10.0.3.tgz",
|
||||
"integrity": "sha512-RWwIT2UBAIwf3f6DQtt5gyjxHMRJoeO9TQku+ueR8dBMakqSSe8vFwQNfjXEoe0W+Tez5HZCTkZKNMulv3Z+9A==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.3.1",
|
||||
"jss": "^10.0.3"
|
||||
}
|
||||
},
|
||||
"jss-plugin-vendor-prefixer": {
|
||||
"version": "10.0.3",
|
||||
"resolved": "https://registry.npmjs.org/jss-plugin-vendor-prefixer/-/jss-plugin-vendor-prefixer-10.0.3.tgz",
|
||||
"integrity": "sha512-zVs6e5z4tFRK/fJ5kuTLzXlTFQbLeFTVwk7lTZiYNufmZwKT0kSmnOJDUukcSe7JLGSRztjWhnHB/6voP174gw==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.3.1",
|
||||
"css-vendor": "^2.0.7",
|
||||
"jss": "^10.0.3"
|
||||
}
|
||||
},
|
||||
"kind-of": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz",
|
||||
@ -4227,6 +4451,11 @@
|
||||
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
|
||||
"dev": true
|
||||
},
|
||||
"normalize-scroll-left": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/normalize-scroll-left/-/normalize-scroll-left-0.2.0.tgz",
|
||||
"integrity": "sha512-t5oCENZJl8TGusJKoCJm7+asaSsPuNmK6+iEjrZ5TyBj2f02brCRsd4c83hwtu+e5d4LCSBZ0uoDlMjBo+A8yA=="
|
||||
},
|
||||
"npm-run-path": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz",
|
||||
@ -4525,6 +4754,11 @@
|
||||
"find-up": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"popper.js": {
|
||||
"version": "1.16.1",
|
||||
"resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1.tgz",
|
||||
"integrity": "sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ=="
|
||||
},
|
||||
"posix-character-classes": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz",
|
||||
@ -4798,6 +5032,17 @@
|
||||
"tiny-warning": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"react-transition-group": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.3.0.tgz",
|
||||
"integrity": "sha512-1qRV1ZuVSdxPlPf4O8t7inxUGpdyO5zG9IoNfJxSO0ImU2A1YWkEQvFPuIPZmMLkg5hYs7vv5mMOyfgSkvAwvw==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.5.5",
|
||||
"dom-helpers": "^5.0.1",
|
||||
"loose-envify": "^1.4.0",
|
||||
"prop-types": "^15.6.2"
|
||||
}
|
||||
},
|
||||
"readable-stream": {
|
||||
"version": "2.3.6",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
|
||||
|
@ -5,7 +5,7 @@
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"build": "webpack --config webpack.prod.js",
|
||||
"build": "webpack --config webpack.prod.js --env production",
|
||||
"devbuild": "webpack --config webpack.dev.js"
|
||||
},
|
||||
"repository": {
|
||||
@ -19,6 +19,8 @@
|
||||
},
|
||||
"homepage": "https://github.com/Sarsoo/spotify-web#readme",
|
||||
"dependencies": {
|
||||
"@material-ui/core": "^4.9.0",
|
||||
"@material-ui/icons": "^4.5.1",
|
||||
"axios": "^0.19.2",
|
||||
"chart.js": "^2.9.3",
|
||||
"react": "^16.12.0",
|
||||
|
@ -1,28 +0,0 @@
|
||||
import React, { Component } from "react";
|
||||
import { BrowserRouter as Router, Route, Link } from "react-router-dom";
|
||||
const axios = require('axios');
|
||||
|
||||
import Lock from "./Lock.js";
|
||||
import Functions from "./Functions.js";
|
||||
import Tasks from "./Tasks.js";
|
||||
|
||||
class Admin extends Component {
|
||||
render(){
|
||||
return (
|
||||
<div>
|
||||
<ul className="navbar" style={{width: "100%"}}>
|
||||
<li><Link to={`${this.props.match.url}/lock`}>Lock Accounts</Link></li>
|
||||
<li><Link to={`${this.props.match.url}/functions`}>Functions</Link></li>
|
||||
<li><Link to={`${this.props.match.url}/tasks`}>Tasks</Link></li>
|
||||
</ul>
|
||||
|
||||
<Route path={`${this.props.match.url}/lock`} component={Lock} />
|
||||
<Route path={`${this.props.match.url}/functions`} component={Functions} />
|
||||
<Route path={`${this.props.match.url}/tasks`} component={Tasks} />
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Admin
|
52
src/js/Admin/AdminRouter.js
Normal file
52
src/js/Admin/AdminRouter.js
Normal file
@ -0,0 +1,52 @@
|
||||
import React, { Component } from "react";
|
||||
import { Route, Link, Switch } from "react-router-dom";
|
||||
import { Paper, Tabs, Tab} from '@material-ui/core';
|
||||
|
||||
|
||||
import Lock from "./Lock.js";
|
||||
import Functions from "./Functions.js";
|
||||
import Tasks from "./Tasks.js";
|
||||
|
||||
class Admin extends Component {
|
||||
|
||||
constructor(props){
|
||||
super(props);
|
||||
this.state = {
|
||||
tab: 0
|
||||
}
|
||||
this.handleChange = this.handleChange.bind(this);
|
||||
}
|
||||
|
||||
handleChange(e, newValue){
|
||||
this.setState({
|
||||
tab: newValue
|
||||
});
|
||||
}
|
||||
|
||||
render(){
|
||||
return (
|
||||
<div>
|
||||
<Paper>
|
||||
<Tabs
|
||||
value={this.state.tab}
|
||||
onChange={this.handleChange}
|
||||
indicatorColor="primary"
|
||||
centered
|
||||
width="50%"
|
||||
>
|
||||
<Tab label="Lock Accounts" component={Link} to={`${this.props.match.url}/lock`} />
|
||||
<Tab label="Functions" component={Link} to={`${this.props.match.url}/functions`} />
|
||||
<Tab label="Tasks" component={Link} to={`${this.props.match.url}/tasks`} />
|
||||
</Tabs>
|
||||
</Paper>
|
||||
<Switch>
|
||||
<Route path={`${this.props.match.url}/lock`} component={Lock} />
|
||||
<Route path={`${this.props.match.url}/functions`} component={Functions} />
|
||||
<Route path={`${this.props.match.url}/tasks`} component={Tasks} />
|
||||
</Switch>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Admin
|
@ -2,6 +2,7 @@ import React, { Component } from "react";
|
||||
const axios = require('axios');
|
||||
|
||||
import showMessage from "../Toast.js"
|
||||
import { Card, Button, ButtonGroup, CardContent, CardActions, Typography } from "@material-ui/core";
|
||||
|
||||
class Functions extends Component {
|
||||
|
||||
@ -34,27 +35,20 @@ class Functions extends Component {
|
||||
|
||||
render () {
|
||||
return (
|
||||
<table className="app-table max-width">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<h1 className="text-no-select full-width center-text ui-text">Admin Functions</h1>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<button className="full-width button" onClick={this.runAllUsers}>Run All Users</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<button className="full-width button" onClick={this.runStats}>Run Stats</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>);
|
||||
<div style={{maxWidth: '1000px', margin: 'auto', marginTop: '20px'}}>
|
||||
<Card align="center">
|
||||
<CardContent>
|
||||
<Typography variant="h4" color="textPrimary">Admin Functions</Typography>
|
||||
</CardContent>
|
||||
<CardActions>
|
||||
<ButtonGroup variant="contained" color="primary" className="full-width">
|
||||
<Button className="full-width button" onClick={this.runAllUsers}>Run All Users</Button>
|
||||
<Button className="full-width button" onClick={this.runStats}>Run Stats</Button>
|
||||
</ButtonGroup>
|
||||
</CardActions>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,8 +1,18 @@
|
||||
import React, { Component } from "react";
|
||||
const axios = require('axios');
|
||||
|
||||
import { Card, Button, CardActions, CardContent, Typography, Grid } from '@material-ui/core';
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
|
||||
import showMessage from "../Toast.js"
|
||||
|
||||
const useStyles = makeStyles({
|
||||
root: {
|
||||
background: '#9e9e9e',
|
||||
color: '#212121'
|
||||
},
|
||||
});
|
||||
|
||||
class Lock extends Component {
|
||||
|
||||
constructor(props){
|
||||
@ -47,41 +57,37 @@ class Lock extends Component {
|
||||
const loadingMessage = <p className="center-text text-no-select">loading...</p>;
|
||||
|
||||
return this.state.isLoading ? loadingMessage :
|
||||
<div>
|
||||
<table className="app-table max-width">
|
||||
<thead>
|
||||
<tr>
|
||||
<th colSpan='3'>
|
||||
<h1 className="text-no-select">
|
||||
Account Locks
|
||||
</h1>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{ this.state.accounts.map((account) => <Row account={account} handler={this.handleLock}
|
||||
key= {account.username}/>) }
|
||||
</tbody>
|
||||
</table>
|
||||
</div>;
|
||||
<div style={{maxWidth: '1000px', margin: 'auto', marginTop: '20px'}}>
|
||||
<Card align="center">
|
||||
<CardContent>
|
||||
<Typography variant="h4" color="textPrimary">Account Locks</Typography>
|
||||
<Grid container spacing={3}>
|
||||
{ this.state.accounts.map((account) => <Row account={account} handler={this.handleLock}
|
||||
key= {account.username}/>) }
|
||||
</Grid>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
function Row(props){
|
||||
const classes = useStyles();
|
||||
return (
|
||||
<tr>
|
||||
<td className="ui-text center-text text-no-select" style={{width: "40%"}}>{ props.account.username }</td>
|
||||
<td className="ui-text center-text text-no-select" style={{width: "30%"}}>
|
||||
{ props.account.last_login }
|
||||
</td>
|
||||
<td style={{width: "30%"}}>
|
||||
<button className="button full-width"
|
||||
onClick={(e) => props.handler(e, props.account.username, !props.account.locked)}>
|
||||
{props.account.locked ? "Unlock" : "Lock"}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<Grid item xs={12} sm={3} md={2}>
|
||||
<Card variant="outlined" className={classes.root}>
|
||||
<CardContent>
|
||||
<Typography variant="h5" color="textSecondary" className={classes.root}>{ props.account.username }</Typography>
|
||||
<Typography variant="body2" color="textSecondary" className={classes.root}>{ props.account.last_login }</Typography>
|
||||
</CardContent>
|
||||
<CardActions>
|
||||
<Button className="full-width" color="secondary" variant="contained" aria-label="delete" onClick={(e) => props.handler(e, props.account.username, !props.account.locked)}>
|
||||
{props.account.locked ? "Unlock" : "Lock"}
|
||||
</Button>
|
||||
</CardActions>
|
||||
</Card>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,8 @@
|
||||
import React, { Component } from "react";
|
||||
const axios = require('axios');
|
||||
|
||||
import { Card, CardContent, Typography, Grid } from '@material-ui/core';
|
||||
|
||||
import showMessage from "../Toast.js"
|
||||
|
||||
class Tasks extends Component {
|
||||
@ -31,40 +33,25 @@ class Tasks extends Component {
|
||||
|
||||
render () {
|
||||
return (
|
||||
<table className="app-table max-width">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<h1 className="text-no-select full-width center-text ui-text">Running Tasks</h1>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
{ this.state.tasks.map((entry) => <TaskType url={entry.url} count={entry.count} times={entry.scheduled_times} key={entry.url}/>)}
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className="text-no-select full-width center-text ui-text" colSpan='2'>
|
||||
<b>{this.state.total_tasks}</b> Currently Running
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>);
|
||||
<div style={{maxWidth: '1000px', margin: 'auto', marginTop: '20px'}}>
|
||||
<Grid container spacing={4}>
|
||||
{ this.state.tasks.map((entry) => <TaskType url={entry.url} count={entry.count} times={entry.scheduled_times} key={entry.url}/>)}
|
||||
</Grid>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function TaskType(props) {
|
||||
return (
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className="text-no-select full-width center-text ui-text" colSpan='2'>
|
||||
{props.url}: {props.count}
|
||||
</td>
|
||||
</tr>
|
||||
{props.times.map((entry) => <tr key={entry}>
|
||||
<td colSpan='2' className="text-no-select full-width center-text ui-text">
|
||||
{entry}
|
||||
</td>
|
||||
</tr>)}
|
||||
</tbody>
|
||||
<Grid item xs={12} sm={6} md={4}>
|
||||
<Card align="center">
|
||||
<CardContent>
|
||||
<Typography variant="body2" color="textSecondary">{props.url}: {props.count}</Typography>
|
||||
{props.times.map((entry) => <Typography variant="body2" color="textSecondary">{entry}</Typography>)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
import React, { Component } from "react";
|
||||
const axios = require('axios');
|
||||
|
||||
import { Card, CardContent, Typography, Grid } from '@material-ui/core';
|
||||
|
||||
class Index extends Component{
|
||||
|
||||
@ -10,37 +11,26 @@ class Index extends Component{
|
||||
|
||||
render(){
|
||||
return (
|
||||
<table className="app-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<h1 className="center-text text-no-select">Music Tools</h1>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className="center-text text-no-select ui-text" style={{fontSize: "20px"}}>
|
||||
Construct spotify playlists from selections of other playlists
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="center-text text-no-select ui-text">
|
||||
Group sub-genre playlists
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="center-text text-no-select ui-text">
|
||||
Optionally append auto-generated recommendations
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="center-text text-no-select ui-text">
|
||||
<br></br>Playlists are run multiple times a day
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div style={{maxWidth: '500px', margin: 'auto', marginTop: '20px'}}>
|
||||
<Card align="center">
|
||||
<CardContent>
|
||||
<Grid container>
|
||||
<Grid item xs={12}>
|
||||
<Typography variant="body1">Construct spotify playlists from selections of other playlists</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<Typography variant="body1">Group sub-genre playlists</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<Typography variant="body1">Optionally append auto-generated recommendations</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<Typography variant="body1">Playlists are run multiple times a day</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,24 +0,0 @@
|
||||
import React, { Component } from "react";
|
||||
import { BrowserRouter as Router, Route, Link, Switch, Redirect} from "react-router-dom";
|
||||
|
||||
import Count from "./Count.js";
|
||||
|
||||
class Maths extends Component {
|
||||
|
||||
render() {
|
||||
return (
|
||||
|
||||
<div>
|
||||
<ul className="navbar" style={{width: "100%"}}>
|
||||
<li><Link to={`${this.props.match.url}/count`}>count</Link></li>
|
||||
</ul>
|
||||
|
||||
<Route path={`${this.props.match.url}/count`} render={(props) => <Count {...props} name={this.props.match.params.name}/>} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
export default Maths;
|
15
src/js/Maths/MathsRouter.js
Normal file
15
src/js/Maths/MathsRouter.js
Normal file
@ -0,0 +1,15 @@
|
||||
import React, { Component } from "react";
|
||||
import { BrowserRouter as Route} from "react-router-dom";
|
||||
|
||||
import Count from "./Count.js";
|
||||
|
||||
class Maths extends Component {
|
||||
|
||||
render() {
|
||||
return <Route path={`${this.props.match.url}/count`} render={(props) => <Count {...props} name={this.props.match.params.name}/>} />;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
export default Maths;
|
@ -21,7 +21,10 @@ class PieChart extends Component {
|
||||
},
|
||||
options: {
|
||||
legend : {
|
||||
display : true
|
||||
display : true,
|
||||
labels: {
|
||||
fontColor: 'white'
|
||||
}
|
||||
},
|
||||
elements: {
|
||||
arc : {
|
||||
|
@ -1,27 +1,48 @@
|
||||
import React, { Component } from "react";
|
||||
import { BrowserRouter as Router, Route, Link, Switch, Redirect} from "react-router-dom";
|
||||
|
||||
import Index from "./Index/Index.js";
|
||||
import Maths from "./Maths/Maths.js";
|
||||
import Playlists from "./Playlist/Playlists.js";
|
||||
import PlaylistView from "./Playlist/View/View.js";
|
||||
import Settings from "./Settings/Settings.js";
|
||||
import Admin from "./Admin/Admin.js";
|
||||
import { BrowserRouter as Router, Route, Link, Switch } from "react-router-dom";
|
||||
|
||||
import NotFound from "./Error/NotFound.js";
|
||||
|
||||
import showMessage from "./Toast.js"
|
||||
|
||||
import GlobalTheme from './Theme.js';
|
||||
|
||||
import { Typography } from '@material-ui/core';
|
||||
import { ThemeProvider } from '@material-ui/core/styles';
|
||||
import AppBar from '@material-ui/core/AppBar';
|
||||
import Toolbar from '@material-ui/core/Toolbar';
|
||||
import IconButton from '@material-ui/core/IconButton';
|
||||
import MenuIcon from '@material-ui/icons/Menu';
|
||||
|
||||
import Drawer from '@material-ui/core/Drawer';
|
||||
import List from '@material-ui/core/List';
|
||||
import Divider from '@material-ui/core/Divider';
|
||||
import ChevronLeftIcon from '@material-ui/icons/ChevronLeft';
|
||||
import ListItem from '@material-ui/core/ListItem';
|
||||
import ListItemIcon from '@material-ui/core/ListItemIcon';
|
||||
import ListItemText from '@material-ui/core/ListItemText';
|
||||
import HomeIcon from '@material-ui/icons/Home';
|
||||
import { Build, PieChart, QueueMusic, ExitToApp, AccountCircle, KeyboardBackspace } from '@material-ui/icons'
|
||||
|
||||
const axios = require('axios');
|
||||
|
||||
const LazyIndex = React.lazy(() => import("./Index/Index"))
|
||||
const LazyMaths = React.lazy(() => import("./Maths/MathsRouter"))
|
||||
const LazyPlaylists = React.lazy(() => import("./Playlist/AllPlaylistsRouter"))
|
||||
const LazyPlaylistView = React.lazy(() => import("./Playlist/View/PlaylistRouter"))
|
||||
const LazySettings = React.lazy(() => import("./Settings/SettingsRouter"))
|
||||
const LazyAdmin = React.lazy(() => import("./Admin/AdminRouter"))
|
||||
|
||||
class MusicTools extends Component {
|
||||
|
||||
constructor(props){
|
||||
super(props);
|
||||
this.state = {
|
||||
type: null,
|
||||
spotify_linked: null
|
||||
spotify_linked: null,
|
||||
drawerOpen: false
|
||||
}
|
||||
this.setOpen = this.setOpen.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
@ -50,41 +71,99 @@ class MusicTools extends Component {
|
||||
});
|
||||
}
|
||||
|
||||
setOpen(bool){
|
||||
this.setState({
|
||||
drawerOpen: bool
|
||||
})
|
||||
}
|
||||
|
||||
render(){
|
||||
return (
|
||||
<Router>
|
||||
<div className="card pad-12">
|
||||
<table className="sidebar pad-3">
|
||||
<tbody>
|
||||
<tr><td><span><Link to="/app">Home</Link></span></td></tr>
|
||||
<tr><td><Link to="/app/playlists">Playlists</Link></td></tr>
|
||||
<tr><td><Link to="/app/maths/count">Maths</Link></td></tr>
|
||||
<tr><td><Link to="/app/settings/password">Settings</Link></td></tr>
|
||||
{ this.state.type == 'admin' && <tr><td><Link to="/app/admin/lock">Admin</Link></td></tr> }
|
||||
<tr><td><a href="/auth/logout">Logout</a></td></tr>
|
||||
<tr><td><a href="https://sarsoo.xyz">sarsoo.xyz</a></td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div className="pad-9">
|
||||
<ThemeProvider theme={GlobalTheme}>
|
||||
<AppBar position="static">
|
||||
<Toolbar>
|
||||
<IconButton edge="start" color="inherit" aria-label="menu" onClick={(e) => this.setOpen(true)}>
|
||||
<MenuIcon />
|
||||
</IconButton>
|
||||
<Typography variant="h6">
|
||||
<Link to='/app/playlists' style={{textDecoration: 'none'}}>Music Tools</Link>
|
||||
</Typography>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
<Drawer
|
||||
variant="persistent"
|
||||
anchor="left"
|
||||
open={this.state.drawerOpen}
|
||||
onClose={(e) => this.setOpen(false)}
|
||||
>
|
||||
<div>
|
||||
<IconButton onClick={(e) => this.setOpen(false)}>
|
||||
<ChevronLeftIcon />
|
||||
</IconButton>
|
||||
</div>
|
||||
<Divider />
|
||||
<div
|
||||
role="presentation"
|
||||
onClick={(e) => this.setOpen(false)}
|
||||
onKeyDown={(e) => this.setOpen(false)}
|
||||
>
|
||||
<List>
|
||||
<ListItem button key="home" component={Link} to='/app'>
|
||||
<ListItemIcon><HomeIcon /></ListItemIcon>
|
||||
<ListItemText primary="Home" />
|
||||
</ListItem>
|
||||
<ListItem button key="playlists" component={Link} to='/app/playlists'>
|
||||
<ListItemIcon><QueueMusic /></ListItemIcon>
|
||||
<ListItemText primary="Playlists" />
|
||||
</ListItem>
|
||||
<ListItem button key="maths" component={Link} to='/app/maths/count'>
|
||||
<ListItemIcon><PieChart /></ListItemIcon>
|
||||
<ListItemText primary="Maths" />
|
||||
</ListItem>
|
||||
<ListItem button key="settings" component={Link} to='/app/settings/password'>
|
||||
<ListItemIcon><Build /></ListItemIcon>
|
||||
<ListItemText primary="Settings" />
|
||||
</ListItem>
|
||||
{ this.state.type == 'admin' &&
|
||||
<ListItem button key="admin" component={Link} to='/app/admin/lock'>
|
||||
<ListItemIcon><AccountCircle /></ListItemIcon>
|
||||
<ListItemText primary="Admin" />
|
||||
</ListItem>
|
||||
}
|
||||
<ListItem button key="logout" component={Link} to='/auth/logout'>
|
||||
<ListItemIcon><KeyboardBackspace /></ListItemIcon>
|
||||
<ListItemText primary="Logout" />
|
||||
</ListItem>
|
||||
<ListItem button key="sarsoo.xyz" component={Link} to='https://sarsoo.xyz'>
|
||||
<ListItemIcon><ExitToApp /></ListItemIcon>
|
||||
<ListItemText primary="sarsoo.xyz" />
|
||||
</ListItem>
|
||||
</List>
|
||||
</div>
|
||||
</Drawer>
|
||||
<div className="full-width">
|
||||
<Switch>
|
||||
<Route path="/app" exact component={Index} />
|
||||
<Route path="/app/playlists" component={Playlists} />
|
||||
<Route path="/app/maths" component={Maths} />
|
||||
<Route path="/app/settings" component={Settings} />
|
||||
{ this.state.type == 'admin' && <Route path="/app/admin" component={Admin} /> }
|
||||
<Route path='/app/playlist/:name' component={PlaylistView} />
|
||||
<React.Suspense fallback={<LoadingMessage/>}>
|
||||
<Route path="/app" exact component={LazyIndex} />
|
||||
<Route path="/app/playlists" component={LazyPlaylists} />
|
||||
<Route path="/app/maths" component={LazyMaths} />
|
||||
<Route path="/app/settings" component={LazySettings} />
|
||||
{ this.state.type == 'admin' && <Route path="/app/admin" component={LazyAdmin} /> }
|
||||
<Route path='/app/playlist/:name' component={LazyPlaylistView} />
|
||||
</React.Suspense>
|
||||
<Route component={NotFound} />
|
||||
</Switch>
|
||||
</div>
|
||||
</div>
|
||||
<footer>
|
||||
<a href="https://github.com/Sarsoo/spotify-web">view source code</a>
|
||||
</footer>
|
||||
</ThemeProvider>
|
||||
</Router>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function LoadingMessage(props) {
|
||||
return <ThemeProvider theme={GlobalTheme}><Typography variant="h5" component="h2" className="ui-text center-text">Loading...</Typography></ThemeProvider>;
|
||||
}
|
||||
|
||||
export default MusicTools;
|
@ -1,20 +1,14 @@
|
||||
import React, { Component } from "react";
|
||||
import { BrowserRouter as Router, Route, Link, Switch } from "react-router-dom";
|
||||
const axios = require('axios');
|
||||
import { Route, Switch } from "react-router-dom";
|
||||
|
||||
import PlaylistsView from "./PlaylistsView.js"
|
||||
import NewPlaylist from "./NewPlaylist.js";
|
||||
import PlaylistsView from "./PlaylistsList.js"
|
||||
import NewPlaylist from "./New.js";
|
||||
import ScratchView from "./ScratchView.js";
|
||||
|
||||
class Playlists extends Component {
|
||||
render(){
|
||||
return (
|
||||
<div>
|
||||
<ul className="navbar" style={{width: "100%"}}>
|
||||
<li><Link to={`${this.props.match.url}/new`}>New</Link></li>
|
||||
<li><Link to={`${this.props.match.url}/play`}>Play</Link></li>
|
||||
</ul>
|
||||
|
||||
<Switch>
|
||||
<Route exact path={`${this.props.match.url}/`} component={PlaylistsView} />
|
||||
<Route path={`${this.props.match.url}/new`} component={NewPlaylist} />
|
@ -1,7 +1,8 @@
|
||||
import React, { Component } from "react";
|
||||
import { BrowserRouter as Redirect } from "react-router-dom";
|
||||
const axios = require('axios');
|
||||
|
||||
import { Card, Button, FormControl, TextField, InputLabel, Select, CardActions, CardContent, Typography, Grid } from '@material-ui/core';
|
||||
|
||||
import showMessage from "../Toast.js"
|
||||
|
||||
class NewPlaylist extends Component {
|
||||
@ -82,44 +83,51 @@ class NewPlaylist extends Component {
|
||||
|
||||
render(){
|
||||
return (
|
||||
<table className="app-table max-width">
|
||||
<thead>
|
||||
<tr>
|
||||
<th colSpan="2">
|
||||
<h1 className="ui-text center-text text-no-select">New Playlist</h1>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<select className="full-width" name="type" onChange={this.handleInputChange}>
|
||||
<option value="default">Default</option>
|
||||
<option value="recents">Recents</option>
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
className="full-width"
|
||||
name="name"
|
||||
type="text"
|
||||
value={this.state.name}
|
||||
<div style={{maxWidth: '500px', margin: 'auto', marginTop: '20px'}}>
|
||||
<Card align="center">
|
||||
<CardContent>
|
||||
<Grid container spacing={5}>
|
||||
<Grid item xs={12}>
|
||||
<Typography variant="h3">New</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={4}>
|
||||
<FormControl variant="filled">
|
||||
<InputLabel htmlFor="type-select">Type</InputLabel>
|
||||
<Select
|
||||
native
|
||||
value={this.state.type}
|
||||
onChange={this.handleInputChange}
|
||||
placeholder="Name"/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colSpan="2">
|
||||
<input type="submit" className="button full-width" onClick={this.handleSubmit} value="Create" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colSpan="2" className="ui-text text-no-select center-text">
|
||||
<br></br>{this.state.description}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
inputProps={{
|
||||
name: 'type',
|
||||
id: 'type-select',
|
||||
}}
|
||||
className="full-width"
|
||||
>
|
||||
<option value="default">Default</option>
|
||||
<option value="recents">Recents</option>
|
||||
<option value="fmchart">Last.fm Chart</option>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={8}>
|
||||
<TextField
|
||||
label="Name"
|
||||
variant="outlined"
|
||||
onChange={this.handleInputChange}
|
||||
name="name"
|
||||
value={this.state.name}
|
||||
className="full-width" />
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<Typography variant="body2" color="textSecondary">{ this.state.description }</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</CardContent>
|
||||
<CardActions>
|
||||
<Button variant="contained" color="primary" className="full-width" onClick={this.handleSubmit}>Create</Button>
|
||||
</CardActions>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1,5 +1,8 @@
|
||||
import React, { Component } from "react";
|
||||
import { BrowserRouter as Router, Route, Link } from "react-router-dom";
|
||||
import { Link } from "react-router-dom";
|
||||
import { Button, ButtonGroup, Typography, Card, Grid, CircularProgress } from '@material-ui/core';
|
||||
import CardActions from '@material-ui/core/CardActions';
|
||||
import CardContent from '@material-ui/core/CardContent';
|
||||
const axios = require('axios');
|
||||
|
||||
import showMessage from "../Toast.js"
|
||||
@ -90,58 +93,66 @@ class PlaylistsView extends Component {
|
||||
|
||||
render() {
|
||||
|
||||
const table = <Table playlists={this.state.playlists}
|
||||
const grid = <PlaylistGrid playlists={this.state.playlists}
|
||||
handleRunPlaylist={this.handleRunPlaylist}
|
||||
handleDeletePlaylist={this.handleDeletePlaylist}
|
||||
handleRunAll={this.handleRunAll}/>;
|
||||
|
||||
const loadingMessage = <p className="center-text">loading...</p>;
|
||||
|
||||
return this.state.isLoading ? loadingMessage : table;
|
||||
return this.state.isLoading ? <CircularProgress /> : grid;
|
||||
}
|
||||
}
|
||||
|
||||
function Table(props){
|
||||
function PlaylistGrid(props){
|
||||
return (
|
||||
<table className="app-table max-width">
|
||||
<Grid container
|
||||
spacing={3}
|
||||
direction="row"
|
||||
justify="flex-start"
|
||||
alignItems="flex-start"
|
||||
style={{padding: '24px'}}>
|
||||
<Grid item xs>
|
||||
<ButtonGroup
|
||||
color="primary"
|
||||
orientation="vertical"
|
||||
className="full-width">
|
||||
<Button component={Link} to='playlists/new' >New</Button>
|
||||
<Button onClick={props.handleRunAll}>Run All</Button>
|
||||
</ButtonGroup>
|
||||
</Grid>
|
||||
{ props.playlists.length == 0 ? (
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className="ui-text text-no-select center-text">
|
||||
No Playlists
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<Grid item xs={12} sm={6} md={3}>
|
||||
<Typography variant="h5" component="h2">No Playlists</Typography>
|
||||
</Grid>
|
||||
) : (
|
||||
<tbody>
|
||||
{ props.playlists.map((playlist) => <Row playlist={ playlist }
|
||||
props.playlists.map((playlist) => <PlaylistCard playlist={ playlist }
|
||||
handleRunPlaylist={props.handleRunPlaylist}
|
||||
handleDeletePlaylist={props.handleDeletePlaylist}
|
||||
key={ playlist.name }/>) }
|
||||
<tr>
|
||||
<td colSpan="3"><button className="full-width button" onClick={props.handleRunAll}>Run All</button></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
key={ playlist.name }/>)
|
||||
)}
|
||||
</table>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
|
||||
function Row(props){
|
||||
function PlaylistCard(props){
|
||||
return (
|
||||
<tr>
|
||||
<PlaylistLink playlist={props.playlist}/>
|
||||
<td style={{width: "100px"}}><button className="button" style={{width: "100px"}} onClick={(e) => props.handleRunPlaylist(props.playlist.name, e)}>Run</button></td>
|
||||
<td style={{width: "100px"}}><button className="button" style={{width: "100px"}} onClick={(e) => props.handleDeletePlaylist(props.playlist.name, e)}>Delete</button></td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
function PlaylistLink(props){
|
||||
return (
|
||||
<td>
|
||||
<Link to={ getPlaylistLink(props.playlist.name) } className="button full-width">{ props.playlist.name }</Link>
|
||||
</td>
|
||||
<Grid item xs>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Typography variant="h5" component="h2">
|
||||
{ props.playlist.name }
|
||||
</Typography>
|
||||
</CardContent>
|
||||
<CardActions>
|
||||
<ButtonGroup
|
||||
color="primary"
|
||||
variant="contained">
|
||||
<Button component={Link} to={getPlaylistLink(props.playlist.name)}>View</Button>
|
||||
<Button onClick={(e) => props.handleRunPlaylist(props.playlist.name, e)}>Run</Button>
|
||||
<Button onClick={(e) => props.handleDeletePlaylist(props.playlist.name, e)}>Delete</Button>
|
||||
</ButtonGroup>
|
||||
</CardActions>
|
||||
</Card>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
|
@ -1,10 +1,13 @@
|
||||
import React, { Component } from "react";
|
||||
const axios = require('axios');
|
||||
|
||||
import showMessage from "../../Toast.js"
|
||||
import PieChart from "../../Maths/PieChart.js";
|
||||
import { Card, Button, CardActions, CardContent, Typography, Grid } from '@material-ui/core';
|
||||
|
||||
class Count extends Component {
|
||||
import showMessage from "../../Toast.js"
|
||||
|
||||
const LazyPieChart = React.lazy(() => import("../../Maths/PieChart"))
|
||||
|
||||
export class Count extends Component {
|
||||
|
||||
constructor(props){
|
||||
super(props);
|
||||
@ -64,34 +67,35 @@ class Count extends Component {
|
||||
|
||||
render() {
|
||||
return (
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className="ui-text center-text text-no-select">Scrobble Count: <b>{this.state.playlist.lastfm_stat_count.toLocaleString()} / {this.state.playlist.lastfm_stat_percent}%</b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="ui-text center-text text-no-select">Album Count: <b>{this.state.playlist.lastfm_stat_album_count.toLocaleString()} / {this.state.playlist.lastfm_stat_album_percent}%</b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="ui-text center-text text-no-select">Artist Count: <b>{this.state.playlist.lastfm_stat_artist_count.toLocaleString()} / {this.state.playlist.lastfm_stat_artist_percent}%</b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="ui-text center-text text-no-select">Last Updated <b>{this.state.playlist.lastfm_stat_last_refresh.toLocaleString()}</b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<PieChart data={[{
|
||||
"label": `${this.state.playlist.name} Tracks`,
|
||||
"value": this.state.playlist.lastfm_stat_percent
|
||||
},{
|
||||
"label": 'Other',
|
||||
"value": 100 - this.state.playlist.lastfm_stat_percent
|
||||
}]}
|
||||
title={this.state.playlist.name}/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<PieChart data={[{
|
||||
<div style={{margin: 'auto', marginTop: '20px'}}>
|
||||
<Card align="center">
|
||||
<CardContent>
|
||||
<Grid container>
|
||||
<Grid item xs={12}>
|
||||
<Typography variant="body2">Scrobble Count: <b>{this.state.playlist.lastfm_stat_count.toLocaleString()} / {this.state.playlist.lastfm_stat_percent}%</b></Typography>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<Typography variant="body2">Album Count: <b>{this.state.playlist.lastfm_stat_album_count.toLocaleString()} / {this.state.playlist.lastfm_stat_album_percent}%</b></Typography>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<Typography variant="body2">Artist Count: <b>{this.state.playlist.lastfm_stat_artist_count.toLocaleString()} / {this.state.playlist.lastfm_stat_artist_percent}%</b></Typography>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<Typography variant="body2">Last Updated <b>{this.state.playlist.lastfm_stat_last_refresh.toLocaleString()}</b></Typography>
|
||||
</Grid>
|
||||
<React.Suspense fallback={<LoadingMessage/>}>
|
||||
<Grid item xs={12} sm={12} md={4}>
|
||||
<LazyPieChart data={[{
|
||||
"label": `${this.state.playlist.name} Tracks`,
|
||||
"value": this.state.playlist.lastfm_stat_percent
|
||||
},{
|
||||
"label": 'Other',
|
||||
"value": 100 - this.state.playlist.lastfm_stat_percent
|
||||
}]}
|
||||
title={this.state.playlist.name}/>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={12} md={4}>
|
||||
<LazyPieChart data={[{
|
||||
"label": `${this.state.playlist.name} Albums`,
|
||||
"value": this.state.playlist.lastfm_stat_album_percent
|
||||
},{
|
||||
@ -99,28 +103,29 @@ class Count extends Component {
|
||||
"value": 100 - this.state.playlist.lastfm_stat_album_percent
|
||||
}]}
|
||||
title={this.state.playlist.name}/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<PieChart data={[{
|
||||
"label": `${this.state.playlist.name} Artists`,
|
||||
"value": this.state.playlist.lastfm_stat_artist_percent
|
||||
},{
|
||||
"label": 'Other',
|
||||
"value": 100 - this.state.playlist.lastfm_stat_artist_percent
|
||||
}]}
|
||||
title={this.state.playlist.name}/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colSpan="2">
|
||||
<button style={{width: "100%"}} className="button" onClick={this.updateStats}>Update</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={12} md={4}>
|
||||
<LazyPieChart data={[{
|
||||
"label": `${this.state.playlist.name} Artists`,
|
||||
"value": this.state.playlist.lastfm_stat_artist_percent
|
||||
},{
|
||||
"label": 'Other',
|
||||
"value": 100 - this.state.playlist.lastfm_stat_artist_percent
|
||||
}]}
|
||||
title={this.state.playlist.name}/>
|
||||
</Grid>
|
||||
</React.Suspense>
|
||||
</Grid>
|
||||
</CardContent>
|
||||
<CardActions>
|
||||
<Button variant="contained" color="primary" className="full-width" onClick={this.updateStats}>Update</Button>
|
||||
</CardActions>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Count;
|
||||
function LoadingMessage(props) {
|
||||
return <Typography variant="h5" component="h2" className="ui-text center-text">Loading...</Typography>;
|
||||
}
|
@ -1,6 +1,11 @@
|
||||
import React, { Component } from "react";
|
||||
const axios = require('axios');
|
||||
|
||||
import { Card, Button, CircularProgress, FormControl, TextField, InputLabel, Select, Checkbox, FormControlLabel,
|
||||
CardActions, CardContent, Typography, Grid } from '@material-ui/core';
|
||||
import { Delete } from '@material-ui/icons';
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
|
||||
import showMessage from "../../Toast.js";
|
||||
|
||||
var thisMonth = [
|
||||
@ -33,7 +38,15 @@ var lastMonth = [
|
||||
'november'
|
||||
];
|
||||
|
||||
class Edit extends Component{
|
||||
|
||||
const useStyles = makeStyles({
|
||||
root: {
|
||||
background: '#9e9e9e',
|
||||
color: '#212121'
|
||||
},
|
||||
});
|
||||
|
||||
export class Edit extends Component{
|
||||
|
||||
constructor(props){
|
||||
super(props);
|
||||
@ -393,196 +406,168 @@ class Edit extends Component{
|
||||
var date = new Date();
|
||||
|
||||
const table = (
|
||||
<tbody>
|
||||
{ this.state.playlist_references.length > 0 && <tr><td colSpan="2" className="ui-text center-text text-no-select" style={{fontStyle: 'italic'}}>Managed</td></tr> }
|
||||
{ this.state.playlist_references.length > 0 && <ListBlock handler={this.handleRemoveReference} list={this.state.playlist_references}/> }
|
||||
<div style={{maxWidth: '1000px', margin: 'auto', marginTop: '20px'}}>
|
||||
<Card align="center">
|
||||
<CardContent>
|
||||
<Typography variant="h2" color="textPrimary">{this.state.name}</Typography>
|
||||
<Grid container spacing={5}>
|
||||
|
||||
{ this.state.parts.length > 0 && <tr><td colSpan="2" className="ui-text center-text text-no-select" style={{fontStyle: 'italic'}}>Spotify</td></tr> }
|
||||
{ this.state.parts.length > 0 && <ListBlock handler={this.handleRemovePart} list={this.state.parts}/> }
|
||||
<tr>
|
||||
<td colSpan="2" className="center-text ui-text text-no-select" style={{fontStyle: "italic"}}>
|
||||
<br></br>Spotify playlist can be the name of either your own created playlist or one you follow, names are case sensitive
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<input type="text"
|
||||
name="newPlaylistName"
|
||||
className="full-width"
|
||||
value={this.state.newPlaylistName}
|
||||
onChange={this.handleInputChange}
|
||||
placeholder="Spotify Playlist Name"></input>
|
||||
</td>
|
||||
<td>
|
||||
<button className="button full-width" onClick={this.handleAddPart}>Add</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<select name="newReferenceName"
|
||||
className="full-width"
|
||||
value={this.state.newReferenceName}
|
||||
onChange={this.handleInputChange}>
|
||||
{ this.state.playlists
|
||||
.filter((entry) => entry.name != this.state.name)
|
||||
.map((entry) => <ReferenceEntry name={entry.name} key={entry.name} />) }
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<button className="button full-width" onClick={this.handleAddReference}>Add</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="center-text ui-text text-no-select">
|
||||
Shuffle Output
|
||||
</td>
|
||||
<td>
|
||||
<input type="checkbox"
|
||||
checked={this.state.shuffle}
|
||||
onChange={this.handleShuffleChange}></input>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="center-text ui-text text-no-select">
|
||||
Include Recommendations
|
||||
</td>
|
||||
<td>
|
||||
<input type="checkbox"
|
||||
checked={this.state.include_recommendations}
|
||||
onChange={this.handleIncludeRecommendationsChange}></input>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="center-text ui-text text-no-select">
|
||||
Include Library Tracks
|
||||
</td>
|
||||
<td>
|
||||
<input type="checkbox"
|
||||
checked={this.state.include_library_tracks}
|
||||
onChange={this.handleIncludeLibraryTracksChange}></input>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="center-text ui-text text-no-select">
|
||||
Recommendation Size
|
||||
</td>
|
||||
<td>
|
||||
<input type="number"
|
||||
name="recommendation_sample"
|
||||
className="full-width"
|
||||
value={this.state.recommendation_sample}
|
||||
onChange={this.handleInputChange}></input>
|
||||
</td>
|
||||
</tr>
|
||||
{ this.state.playlist_references.length > 0 && <Grid item xs={12} ><Typography color="textSecondary" variant="h4">Managed</Typography></Grid> }
|
||||
{ this.state.playlist_references.length > 0 && <ListBlock handler={this.handleRemoveReference} list={this.state.playlist_references}/> }
|
||||
|
||||
{ this.state.type == 'fmchart' &&
|
||||
<tr>
|
||||
<td className="center-text ui-text text-no-select">
|
||||
Chart Size
|
||||
</td>
|
||||
<td>
|
||||
<input type="number"
|
||||
name="chart_limit"
|
||||
className="full-width"
|
||||
value={this.state.chart_limit}
|
||||
onChange={this.handleInputChange}></input>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
{ this.state.type == 'fmchart' &&
|
||||
<tr>
|
||||
<td className="center-text ui-text text-no-select">
|
||||
Chart Range
|
||||
</td>
|
||||
<td>
|
||||
<select className="full-width"
|
||||
name="chart_range"
|
||||
onChange={this.handleInputChange}
|
||||
value={this.state.chart_range}>
|
||||
<option value="WEEK">7 Day</option>
|
||||
<option value="MONTH">30 Day</option>
|
||||
<option value="QUARTER">90 Day</option>
|
||||
<option value="HALFYEAR">180 Day</option>
|
||||
<option value="YEAR">365 Day</option>
|
||||
<option value="OVERALL">Overall</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
|
||||
{ this.state.type == 'recents' &&
|
||||
<tr>
|
||||
<td className="center-text ui-text text-no-select">
|
||||
Added Since (days)
|
||||
</td>
|
||||
<td>
|
||||
<input type="number"
|
||||
name="day_boundary"
|
||||
className="full-width"
|
||||
value={this.state.day_boundary}
|
||||
onChange={this.handleInputChange}></input>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
{ this.state.type == 'recents' &&
|
||||
<tr>
|
||||
<td className="center-text ui-text text-no-select">
|
||||
Include {thisMonth[date.getMonth()]} Playlist
|
||||
</td>
|
||||
<td>
|
||||
<input type="checkbox"
|
||||
checked={this.state.add_this_month}
|
||||
onChange={this.handleThisMonthChange}></input>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
{ this.state.type == 'recents' &&
|
||||
<tr>
|
||||
<td className="center-text ui-text text-no-select">
|
||||
Include {lastMonth[date.getMonth()]} Playlist
|
||||
</td>
|
||||
<td>
|
||||
<input type="checkbox"
|
||||
checked={this.state.add_last_month}
|
||||
onChange={this.handleLastMonthChange}></input>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
|
||||
|
||||
<tr>
|
||||
<td className="center-text ui-text text-no-select">
|
||||
Type
|
||||
</td>
|
||||
<td>
|
||||
<select className="full-width"
|
||||
name="type"
|
||||
{ this.state.parts.length > 0 && <Grid item xs={12} ><Typography color="textSecondary" variant="h4">Spotify</Typography></Grid> }
|
||||
{ this.state.parts.length > 0 && <ListBlock handler={this.handleRemovePart} list={this.state.parts}/> }
|
||||
<Grid item xs={12} ><Typography variant="body2" color="textSecondary">Spotify playlist can be the name of either your own created playlist or one you follow, names are case sensitive</Typography></Grid>
|
||||
<Grid item xs={8} sm={8} md={3}>
|
||||
<TextField
|
||||
name="newPlaylistName"
|
||||
variant="outlined"
|
||||
label="Spotify Playlist"
|
||||
value={this.state.newPlaylistName}
|
||||
onChange={this.handleInputChange}
|
||||
value={this.state.type}>
|
||||
<option value="default">Default</option>
|
||||
<option value="recents">Recents</option>
|
||||
<option value="fmchart">Last.fm Chart</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colSpan="2">
|
||||
<button className="button full-width" onClick={this.handleRun}>Run</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={4} sm={4} md={3}>
|
||||
<Button variant="contained" className="full-width" onClick={this.handleAddPart} style={{verticalAlign: 'middle'}}>Add</Button>
|
||||
</Grid>
|
||||
<Grid item xs={8} sm={8} md={3}>
|
||||
<FormControl variant="filled" style={{verticalAlign: 'middle'}}>
|
||||
<InputLabel htmlFor="chart_range">Managed Playlist</InputLabel>
|
||||
<Select
|
||||
native
|
||||
value={this.state.newReferenceName}
|
||||
onChange={this.handleInputChange}
|
||||
inputProps={{
|
||||
name: "newReferenceName",
|
||||
id: "newReferenceName",
|
||||
}}
|
||||
>
|
||||
{ this.state.playlists
|
||||
.filter((entry) => entry.name != this.state.name)
|
||||
.map((entry) => <ReferenceEntry name={entry.name} key={entry.name} />) }
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Grid>
|
||||
<Grid item xs={4} sm={4} md={3}>
|
||||
<Button variant="contained" className="full-width" onClick={this.handleAddReference} style={{verticalAlign: 'middle'}}>Add</Button>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox color="primary" checked={this.state.shuffle} onChange={this.handleShuffleChange} />
|
||||
}
|
||||
labelPlacement="bottom"
|
||||
label="Shuffle"/>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox color="primary" checked={this.state.include_recommendations} onChange={this.handleIncludeRecommendationsChange} />
|
||||
}
|
||||
labelPlacement="bottom"
|
||||
label="Recommendations"/>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox color="primary" checked={this.state.include_library_tracks} onChange={this.handleIncludeLibraryTracksChange} />
|
||||
}
|
||||
labelPlacement="bottom"
|
||||
label="Library Tracks"/>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<TextField type="number"
|
||||
name="recommendation_sample"
|
||||
label="Recommendation Size"
|
||||
value={this.state.recommendation_sample}
|
||||
onChange={this.handleInputChange}></TextField>
|
||||
</Grid>
|
||||
|
||||
{ this.state.type == 'fmchart' &&
|
||||
<Grid item xs={12}>
|
||||
<TextField type="number"
|
||||
name="chart_limit"
|
||||
label="Chart Size"
|
||||
value={this.state.chart_limit}
|
||||
onChange={this.handleInputChange}></TextField>
|
||||
</Grid>
|
||||
}
|
||||
{ this.state.type == 'fmchart' &&
|
||||
<Grid item xs={12}>
|
||||
<FormControl variant="filled">
|
||||
<InputLabel htmlFor="chart_range">Chart Range</InputLabel>
|
||||
<Select
|
||||
native
|
||||
value={this.state.chart_range}
|
||||
onChange={this.handleInputChange}
|
||||
inputProps={{
|
||||
name: "chart_range",
|
||||
id: "chart_range",
|
||||
}}
|
||||
>
|
||||
<option value="WEEK">7 Day</option>
|
||||
<option value="MONTH">30 Day</option>
|
||||
<option value="QUARTER">90 Day</option>
|
||||
<option value="HALFYEAR">180 Day</option>
|
||||
<option value="YEAR">365 Day</option>
|
||||
<option value="OVERALL">Overall</option>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Grid>
|
||||
}
|
||||
{ this.state.type == 'recents' &&
|
||||
<Grid item xs={12}>
|
||||
<TextField type="number"
|
||||
name="day_boundary"
|
||||
// className="full-width"
|
||||
label="Added Since (days)"
|
||||
value={this.state.day_boundary}
|
||||
onChange={this.handleInputChange} />
|
||||
</Grid>
|
||||
}
|
||||
{ this.state.type == 'recents' &&
|
||||
<Grid item xs={12}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox color="primary" checked={this.state.add_this_month} onChange={this.handleThisMonthChange} />
|
||||
}
|
||||
label="This Month"
|
||||
labelPlacement="bottom"
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox color="primary" checked={this.state.add_last_month} onChange={this.handleLastMonthChange} />
|
||||
}
|
||||
label="Last Month"
|
||||
labelPlacement="bottom"
|
||||
/>
|
||||
</Grid>
|
||||
}
|
||||
<Grid item xs={12}>
|
||||
<FormControl variant="filled">
|
||||
<InputLabel htmlFor="type-select">Type</InputLabel>
|
||||
<Select
|
||||
native
|
||||
value={this.state.type}
|
||||
onChange={this.handleInputChange}
|
||||
inputProps={{
|
||||
name: 'type',
|
||||
id: 'type-select',
|
||||
}}
|
||||
>
|
||||
<option value="default">Default</option>
|
||||
<option value="recents">Recents</option>
|
||||
<option value="fmchart">Last.fm Chart</option>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</CardContent>
|
||||
<CardActions>
|
||||
<Button onClick={this.handleRun} variant="contained" color="primary" className="full-width" >Run</Button>
|
||||
</CardActions>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
|
||||
const loadingMessage =
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<p className="center-text">Loading...</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>;
|
||||
|
||||
return this.state.isLoading ? loadingMessage : table;
|
||||
return this.state.isLoading ? <CircularProgress /> : table;
|
||||
}
|
||||
|
||||
}
|
||||
@ -592,16 +577,30 @@ function ReferenceEntry(props) {
|
||||
}
|
||||
|
||||
function ListBlock(props) {
|
||||
return props.list.map((part) => <Row part={ part } key={ part } handler={props.handler}/>);
|
||||
return <Grid container
|
||||
spacing={3}
|
||||
direction="row"
|
||||
justify="flex-start"
|
||||
alignItems="flex-start"
|
||||
style={{padding: '24px'}}>
|
||||
{props.list.map((part) => <BlockGridItem part={ part } key={ part } handler={props.handler}/>)}
|
||||
</Grid>
|
||||
}
|
||||
|
||||
function Row (props) {
|
||||
function BlockGridItem (props) {
|
||||
const classes = useStyles();
|
||||
return (
|
||||
<tr>
|
||||
<td className="ui-text center-text text-no-select">{ props.part }</td>
|
||||
<td><button className="ui-text center-text button full-width" onClick={(e) => props.handler(props.part, e)}>Remove</button></td>
|
||||
</tr>
|
||||
<Grid item xs={12} sm={3} md={2}>
|
||||
<Card variant="outlined" className={classes.root}>
|
||||
<CardContent>
|
||||
<Typography variant="h5" color="textSecondary" className={classes.root}>{ props.part }</Typography>
|
||||
</CardContent>
|
||||
<CardActions>
|
||||
<Button className="full-width" color="secondary" variant="contained" aria-label="delete" onClick={(e) => props.handler(props.part, e)} startIcon={<Delete />}>
|
||||
Delete
|
||||
</Button>
|
||||
</CardActions>
|
||||
</Card>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
|
||||
export default Edit;
|
50
src/js/Playlist/View/PlaylistRouter.js
Normal file
50
src/js/Playlist/View/PlaylistRouter.js
Normal file
@ -0,0 +1,50 @@
|
||||
import React, { Component } from "react";
|
||||
import { Route, Link, Switch } from "react-router-dom";
|
||||
import { Paper, Tabs, Tab} from '@material-ui/core';
|
||||
|
||||
|
||||
import {Edit} from "./Edit.js";
|
||||
import {Count} from "./Count.js";
|
||||
|
||||
class View extends Component{
|
||||
|
||||
constructor(props){
|
||||
super(props);
|
||||
this.state = {
|
||||
tab: 0
|
||||
}
|
||||
this.handleChange = this.handleChange.bind(this);
|
||||
}
|
||||
|
||||
handleChange(e, newValue){
|
||||
this.setState({
|
||||
tab: newValue
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<Paper>
|
||||
<Tabs
|
||||
value={this.state.tab}
|
||||
onChange={this.handleChange}
|
||||
indicatorColor="primary"
|
||||
centered
|
||||
width="50%"
|
||||
>
|
||||
<Tab label="Edit" component={Link} to={`${this.props.match.url}/edit`} />
|
||||
<Tab label="Count" component={Link} to={`${this.props.match.url}/count`} />
|
||||
</Tabs>
|
||||
</Paper>
|
||||
<Switch>
|
||||
<Route path={`${this.props.match.url}/edit`} render={(props) => <Edit {...props} name={this.props.match.params.name}/>} />
|
||||
<Route path={`${this.props.match.url}/count`} render={(props) => <Count {...props} name={this.props.match.params.name}/>} />
|
||||
</Switch>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default View;
|
@ -1,36 +0,0 @@
|
||||
import React, { Component } from "react";
|
||||
import { BrowserRouter as Router, Route, Link, Switch, Redirect} from "react-router-dom";
|
||||
const axios = require('axios');
|
||||
|
||||
import Edit from "./Edit.js";
|
||||
import Count from "./Count.js";
|
||||
|
||||
class View extends Component{
|
||||
|
||||
render() {
|
||||
return (
|
||||
<table className="app-table max-width">
|
||||
<thead>
|
||||
<tr>
|
||||
<th colSpan="2"><h1 className="text-no-select">{ this.props.match.params.name }</h1></th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th colSpan="2">
|
||||
<div>
|
||||
<ul className="navbar" style={{width: "100%"}}>
|
||||
<li><Link to={`${this.props.match.url}/edit`}>Edit</Link></li>
|
||||
<li><Link to={`${this.props.match.url}/count`}>Count</Link></li>
|
||||
</ul>
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<Route path={`${this.props.match.url}/edit`} render={(props) => <Edit {...props} name={this.props.match.params.name}/>} />
|
||||
<Route path={`${this.props.match.url}/count`} render={(props) => <Count {...props} name={this.props.match.params.name}/>} />
|
||||
</table>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default View;
|
@ -1,6 +1,8 @@
|
||||
import React, { Component } from "react";
|
||||
const axios = require('axios');
|
||||
|
||||
import { Card, Grid, Button, TextField, CardContent, CardActions, Typography } from "@material-ui/core";
|
||||
|
||||
import showMessage from "../Toast.js"
|
||||
|
||||
class ChangePassword extends Component {
|
||||
@ -63,49 +65,52 @@ class ChangePassword extends Component {
|
||||
|
||||
render(){
|
||||
return (
|
||||
<div>
|
||||
<form onSubmit={this.handleSubmit}>
|
||||
<table className="app-table max-width">
|
||||
<thead>
|
||||
<tr>
|
||||
<th colSpan="2"><h1 className="text-no-select">Change Password</h1></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className="ui-text center-text text-no-select">Current:</td>
|
||||
<td><input
|
||||
<div style={{maxWidth: '500px', margin: 'auto', marginTop: '20px'}}>
|
||||
<Card align="center">
|
||||
<form onSubmit={this.handleSubmit}>
|
||||
<CardContent>
|
||||
<Grid container spacing={2}>
|
||||
<Grid item className="full-width">
|
||||
<Typography variant="h4" color="textPrimary">Change Password</Typography>
|
||||
</Grid>
|
||||
<Grid item className="full-width">
|
||||
<TextField
|
||||
label="Current Password"
|
||||
type="password"
|
||||
variant="outlined"
|
||||
onChange={this.handleCurrentChange}
|
||||
name="current"
|
||||
value={this.state.current}
|
||||
onChange={this.handleCurrentChange}
|
||||
className="full-width" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="ui-text center-text text-no-select">New:</td>
|
||||
<td><input
|
||||
className="full-width" />
|
||||
</Grid>
|
||||
<Grid item className="full-width">
|
||||
<TextField
|
||||
label="New Password"
|
||||
variant="outlined"
|
||||
type="password"
|
||||
onChange={this.handleNewChange}
|
||||
name="new1"
|
||||
value={this.state.new1}
|
||||
onChange={this.handleNewChange}
|
||||
className="full-width" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="ui-text center-text text-no-select">New Again:</td>
|
||||
<td><input
|
||||
className="full-width" />
|
||||
</Grid>
|
||||
<Grid item className="full-width">
|
||||
<TextField
|
||||
label="New Password Again"
|
||||
variant="outlined"
|
||||
type="password"
|
||||
onChange={this.handleNew2Change}
|
||||
name="new2"
|
||||
value={this.state.new2}
|
||||
onChange={this.handleNew2Change}
|
||||
className="full-width" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colSpan="2"><input type="submit" style={{width: "100%"}} className="button" value="Change" /></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</form>
|
||||
{ this.state.error && <p style={{color: "red"}} className="center-text">{this.state.errorValue}</p>}
|
||||
className="full-width" />
|
||||
</Grid>
|
||||
{ this.state.error && <Grid item><Typography variant="textSeondary">{this.state.errorValue}</Typography></Grid>}
|
||||
</Grid>
|
||||
</CardContent>
|
||||
<CardActions>
|
||||
<Button type="submit" variant="contained" className="full-width" onClick={this.runStats}>Change</Button>
|
||||
</CardActions>
|
||||
</form>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1,6 +1,8 @@
|
||||
import React, { Component } from "react";
|
||||
const axios = require('axios');
|
||||
|
||||
import { Card, Button, CardContent, CardActions, Typography, TextField, Grid } from "@material-ui/core";
|
||||
|
||||
import showMessage from "../Toast.js"
|
||||
|
||||
class LastFM extends Component {
|
||||
@ -65,29 +67,31 @@ class LastFM extends Component {
|
||||
|
||||
render(){
|
||||
const table =
|
||||
<form onSubmit={this.handleSubmit}>
|
||||
<table className="app-table max-width">
|
||||
<thead>
|
||||
<tr>
|
||||
<th colSpan="2"><h1 className="ui-text center-text text-no-select">Last.fm Username</h1></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className="ui-text center-text text-no-select">Username:</td>
|
||||
<td><input
|
||||
type="text"
|
||||
name="current"
|
||||
value={this.state.lastfm_username}
|
||||
onChange={this.handleChange}
|
||||
className="full-width" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colSpan="2"><input type="submit" style={{width: "100%"}} className="button" value="save" /></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</form>;
|
||||
<div style={{maxWidth: '400px', margin: 'auto', marginTop: '20px'}}>
|
||||
<Card align="center">
|
||||
<form onSubmit={this.handleSubmit}>
|
||||
<CardContent>
|
||||
<Grid container spacing={3}>
|
||||
<Grid item className="full-width">
|
||||
<Typography variant="h4" color="textPrimary">Last.fm Username</Typography>
|
||||
</Grid>
|
||||
<Grid item className="full-width">
|
||||
<TextField
|
||||
label="last.fm Username"
|
||||
variant="outlined"
|
||||
onChange={this.handleChange}
|
||||
name="current"
|
||||
value={this.state.lastfm_username}
|
||||
className="full-width" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</CardContent>
|
||||
<CardActions>
|
||||
<Button type="submit" variant="contained" className="full-width">Save</Button>
|
||||
</CardActions>
|
||||
</form>
|
||||
</Card>
|
||||
</div>;
|
||||
|
||||
const loadingMessage = <p className="center-text text-no-select">Loading...</p>;
|
||||
|
||||
|
@ -1,30 +0,0 @@
|
||||
import React, { Component } from "react";
|
||||
import { BrowserRouter as Router, Route, Link, Switch, Redirect} from "react-router-dom";
|
||||
|
||||
import ChangePassword from "./ChangePassword.js";
|
||||
import SpotifyLink from "./SpotifyLink.js";
|
||||
import LastFM from "./LastFM.js";
|
||||
|
||||
class Settings extends Component {
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<ul className="navbar" style={{width: "100%"}}>
|
||||
<li><Link to={`${this.props.match.url}/password`}>Password</Link></li>
|
||||
<li><Link to={`${this.props.match.url}/spotify`}>Spotify</Link></li>
|
||||
<li><Link to={`${this.props.match.url}/lastfm`}>Last.fm</Link></li>
|
||||
</ul>
|
||||
|
||||
<Route path={`${this.props.match.url}/password`} component={ChangePassword} />
|
||||
<Route path={`${this.props.match.url}/spotify`} component={SpotifyLink} />
|
||||
<Route path={`${this.props.match.url}/lastfm`} component={LastFM} />
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
export default Settings;
|
53
src/js/Settings/SettingsRouter.js
Normal file
53
src/js/Settings/SettingsRouter.js
Normal file
@ -0,0 +1,53 @@
|
||||
import React, { Component } from "react";
|
||||
import { Route, Link, Switch } from "react-router-dom";
|
||||
import { Paper, Tabs, Tab} from '@material-ui/core';
|
||||
|
||||
import ChangePassword from "./ChangePassword.js";
|
||||
import SpotifyLink from "./SpotifyLink.js";
|
||||
import LastFM from "./LastFM.js";
|
||||
|
||||
class Settings extends Component {
|
||||
|
||||
constructor(props){
|
||||
super(props);
|
||||
this.state = {
|
||||
tab: 0
|
||||
}
|
||||
this.handleChange = this.handleChange.bind(this);
|
||||
}
|
||||
|
||||
handleChange(e, newValue){
|
||||
this.setState({
|
||||
tab: newValue
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<Paper>
|
||||
<Tabs
|
||||
value={this.state.tab}
|
||||
onChange={this.handleChange}
|
||||
indicatorColor="primary"
|
||||
centered
|
||||
width="50%"
|
||||
>
|
||||
<Tab label="Password" component={Link} to={`${this.props.match.url}/password`} />
|
||||
<Tab label="Spotify" component={Link} to={`${this.props.match.url}/spotify`} />
|
||||
<Tab label="Last.fm" component={Link} to={`${this.props.match.url}/lastfm`} />
|
||||
</Tabs>
|
||||
</Paper>
|
||||
<Switch>
|
||||
<Route path={`${this.props.match.url}/password`} component={ChangePassword} />
|
||||
<Route path={`${this.props.match.url}/spotify`} component={SpotifyLink} />
|
||||
<Route path={`${this.props.match.url}/lastfm`} component={LastFM} />
|
||||
</Switch>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
export default Settings;
|
@ -1,6 +1,8 @@
|
||||
import React, { Component } from "react";
|
||||
const axios = require('axios');
|
||||
|
||||
import { Card, Button, CardContent, CardActions, Typography } from "@material-ui/core";
|
||||
|
||||
import showMessage from "../Toast.js"
|
||||
|
||||
class SpotifyLink extends Component {
|
||||
@ -29,25 +31,17 @@ class SpotifyLink extends Component {
|
||||
|
||||
render(){
|
||||
const table =
|
||||
<table className="app-table max-width">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><h1 className="ui-text center-text text-no-select">Spotify Link</h1></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className="ui-text center-text text-no-select">
|
||||
Status: { this.state.spotify_linked ? "Linked" : "Unlinked" }
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
{ this.state.spotify_linked ? <DeAuthButton /> : <AuthButton /> }
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>;
|
||||
<div style={{maxWidth: '400px', margin: 'auto', marginTop: '20px'}}>
|
||||
<Card align="center">
|
||||
<CardContent>
|
||||
<Typography variant="h4" color="textPrimary">Admin Functions</Typography>
|
||||
<Typography variant="body2" color="textSecondary">Status: { this.state.spotify_linked ? "Linked" : "Unlinked" }</Typography>
|
||||
</CardContent>
|
||||
<CardActions>
|
||||
{ this.state.spotify_linked ? <DeAuthButton /> : <AuthButton /> }
|
||||
</CardActions>
|
||||
</Card>
|
||||
</div>;
|
||||
|
||||
const loadingMessage = <p className="center-text text-no-select">Loading...</p>;
|
||||
|
||||
@ -56,11 +50,11 @@ class SpotifyLink extends Component {
|
||||
}
|
||||
|
||||
function AuthButton(props) {
|
||||
return <a className="button full-width" href="/auth/spotify">Auth</a>;
|
||||
return <Button component='a' variant="contained" className="full-width" href="/auth/spotify">Auth</Button>;
|
||||
}
|
||||
|
||||
function DeAuthButton(props) {
|
||||
return <a className="button full-width" href="/auth/spotify/deauth">De-Auth</a>;
|
||||
return <Button component='a' variant="contained" className="full-width" href="/auth/spotify/deauth">De-Auth</Button>;
|
||||
}
|
||||
|
||||
export default SpotifyLink;
|
42
src/js/Theme.js
Normal file
42
src/js/Theme.js
Normal file
@ -0,0 +1,42 @@
|
||||
import { createMuiTheme, responsiveFontSizes } from '@material-ui/core/styles';
|
||||
|
||||
let GlobalTheme = createMuiTheme({
|
||||
root: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
},
|
||||
spacing: 4,
|
||||
typography: {
|
||||
button: {
|
||||
fontSize: '1rem',
|
||||
},
|
||||
},
|
||||
paper: {
|
||||
display: 'flex',
|
||||
spacing: 5
|
||||
},
|
||||
card: {
|
||||
marginTop: 24,
|
||||
display: 'flex',
|
||||
spacing: 5
|
||||
},
|
||||
palette: {
|
||||
type: 'dark',
|
||||
primary: {
|
||||
main: '#1976d2',
|
||||
},
|
||||
secondary: {
|
||||
main: '#dc004e',
|
||||
},
|
||||
error: {
|
||||
main: '#f44336'
|
||||
}
|
||||
},
|
||||
status: {
|
||||
danger: 'orange',
|
||||
}
|
||||
});
|
||||
GlobalTheme = responsiveFontSizes(GlobalTheme);
|
||||
|
||||
export default GlobalTheme;
|
@ -27,7 +27,9 @@ module.exports = {
|
||||
],
|
||||
resolve: { extensions: ["*", ".js", ".jsx"] },
|
||||
output: {
|
||||
filename: '[name].bundle.js',
|
||||
path: path.resolve(__dirname, 'build/js')
|
||||
filename: '[name].bundle.js',
|
||||
chunkFilename: '[name].bundle.js',
|
||||
path: path.resolve(__dirname, 'build/js'),
|
||||
publicPath: '/build/js/'
|
||||
}
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user