mirror of
https://github.com/idanoo/GoScrobble.git
synced 2024-11-21 16:11:56 +00:00
0.1.001 Split out web to separate repo
This commit is contained in:
parent
525d5c92b5
commit
d268d939eb
@ -1,9 +1,8 @@
|
||||
stages:
|
||||
- build
|
||||
- bundle
|
||||
|
||||
variables:
|
||||
VERSION: 0.0.33
|
||||
VERSION: 0.1.001
|
||||
|
||||
build-go:
|
||||
image: golang:1.16.7
|
||||
@ -13,41 +12,9 @@ build-go:
|
||||
script:
|
||||
- go build -o goscrobble cmd/go-scrobble/*.go
|
||||
artifacts:
|
||||
expire_in: 1 day
|
||||
expire_in: 1 week
|
||||
paths:
|
||||
- goscrobble
|
||||
- migrations
|
||||
- init
|
||||
- .env.example
|
||||
|
||||
build-react:
|
||||
image: node:15.12.0
|
||||
stage: build
|
||||
only:
|
||||
- master
|
||||
script:
|
||||
- cd web
|
||||
- npm install
|
||||
- npm run build --env production
|
||||
artifacts:
|
||||
expire_in: 1 day
|
||||
paths:
|
||||
- web/build
|
||||
|
||||
bundle:
|
||||
image: bash:latest
|
||||
stage: bundle
|
||||
only:
|
||||
- master
|
||||
variables:
|
||||
GIT_STRATEGY: none
|
||||
before_script:
|
||||
- apk add --no-cache zip tar
|
||||
script:
|
||||
- zip -r goscrobble.${VERSION}.zip web/build goscrobble migrations init .env.example
|
||||
- tar -czf goscrobble.${VERSION}.tar.gz web/build goscrobble migrations init .env.example
|
||||
artifacts:
|
||||
expire_in: 1 week
|
||||
paths:
|
||||
- goscrobble.${VERSION}.zip
|
||||
- goscrobble.${VERSION}.tar.gz
|
@ -37,6 +37,12 @@ We need to build NPM package, and then ship web/build with the binary.
|
||||
go build -o goscrobble cmd/go-scrobble/*.go
|
||||
./goscrobble
|
||||
|
||||
## Build API Docs
|
||||
cd docs/api && docker run --rm --name slate -v $(pwd)/build:/srv/slate/build -v $(pwd)/source:/srv/slate/source slatedocs/slate build
|
||||
|
||||
|
||||
## Test API Docs
|
||||
cd docs/api && docker run --rm --name slate -p 4567:4567 -v $(pwd)/source:/srv/slate/source slatedocs/slate serve
|
||||
|
||||
## Support Development!
|
||||
Feel free to support hosting and my coffee addiction https://liberapay.com/idanoo
|
@ -99,6 +99,9 @@ func HandleRequests(port string) {
|
||||
spaStatic := spaStaticHandler{staticPath: StaticDirectory}
|
||||
r.PathPrefix("/img").Handler(spaStatic)
|
||||
|
||||
apiDocs := spaStaticHandler{staticPath: "docs/api/build"}
|
||||
r.PathPrefix("/docs").Handler(apiDocs)
|
||||
|
||||
// SERVE FRONTEND - NO AUTH
|
||||
spa := spaHandler{staticPath: "web/build", indexPath: "index.html"}
|
||||
r.PathPrefix("/").Handler(spa)
|
||||
|
@ -1 +0,0 @@
|
||||
REACT_APP_API_URL=http://127.0.0.1:42069
|
23
web/.gitignore
vendored
23
web/.gitignore
vendored
@ -1,23 +0,0 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
@ -1,70 +0,0 @@
|
||||
# Getting Started with Create React App
|
||||
|
||||
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
|
||||
|
||||
## Available Scripts
|
||||
|
||||
In the project directory, you can run:
|
||||
|
||||
### `npm start`
|
||||
|
||||
Runs the app in the development mode.\
|
||||
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
|
||||
|
||||
The page will reload if you make edits.\
|
||||
You will also see any lint errors in the console.
|
||||
|
||||
### `npm test`
|
||||
|
||||
Launches the test runner in the interactive watch mode.\
|
||||
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
|
||||
|
||||
### `npm run build`
|
||||
|
||||
Builds the app for production to the `build` folder.\
|
||||
It correctly bundles React in production mode and optimizes the build for the best performance.
|
||||
|
||||
The build is minified and the filenames include the hashes.\
|
||||
Your app is ready to be deployed!
|
||||
|
||||
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
|
||||
|
||||
### `npm run eject`
|
||||
|
||||
**Note: this is a one-way operation. Once you `eject`, you can’t go back!**
|
||||
|
||||
If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
|
||||
|
||||
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
|
||||
|
||||
You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
|
||||
|
||||
## Learn More
|
||||
|
||||
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
|
||||
|
||||
To learn React, check out the [React documentation](https://reactjs.org/).
|
||||
|
||||
### Code Splitting
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting)
|
||||
|
||||
### Analyzing the Bundle Size
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size)
|
||||
|
||||
### Making a Progressive Web App
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app)
|
||||
|
||||
### Advanced Configuration
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration)
|
||||
|
||||
### Deployment
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment)
|
||||
|
||||
### `npm run build` fails to minify
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)
|
Binary file not shown.
Before Width: | Height: | Size: 4.4 KiB |
39643
web/package-lock.json
generated
39643
web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,56 +0,0 @@
|
||||
{
|
||||
"name": "goscrobble",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@material-ui/core": "^4.11.3",
|
||||
"@reduxjs/toolkit": "^1.5.0",
|
||||
"@testing-library/jest-dom": "^5.11.9",
|
||||
"@testing-library/react": "^11.2.5",
|
||||
"@testing-library/user-event": "^12.8.3",
|
||||
"axios": "^0.21.1",
|
||||
"bootstrap": "^4.6.0",
|
||||
"formik": "^2.2.6",
|
||||
"formik-material-ui": "^3.0.1",
|
||||
"jwt-decode": "^3.1.2",
|
||||
"react": "^17.0.2",
|
||||
"react-bootstrap": "^1.5.2",
|
||||
"react-confirm-alert": "^2.7.0",
|
||||
"react-cookie": "^4.0.3",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"react-scripts": "4.0.3",
|
||||
"react-spinners": "^0.10.6",
|
||||
"react-timezone-select": "^0.10.7",
|
||||
"react-toastify": "^7.0.3",
|
||||
"reactjs-popup": "^2.0.4",
|
||||
"reactstrap": "^8.9.0"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"redux-devtools-extension": "^2.13.9"
|
||||
}
|
||||
}
|
Binary file not shown.
Before Width: | Height: | Size: 1.1 KiB |
@ -1,65 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta
|
||||
name="description"
|
||||
content="GoScrobble.com Open source music scrobbler"
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<style>
|
||||
html, body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
background-color: #282C34;
|
||||
}
|
||||
|
||||
.loader-container {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 25%;
|
||||
transform: translate(-50%, -25%);
|
||||
}
|
||||
|
||||
.loader {
|
||||
border: 16px solid #282C34;
|
||||
border-top: 16px solid #3498db;
|
||||
border-radius: 50%;
|
||||
width: 130px;
|
||||
height: 130px;
|
||||
animation: spin 2s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
</style>
|
||||
<title>GoScrobble</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<div class="loader-container">
|
||||
<div class="loader"></div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
@ -1,52 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="margin: auto; background: rgb(40, 44, 52) none repeat scroll 0% 0%; display: block; shape-rendering: auto;" width="200px" height="200px" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid">
|
||||
<g transform="rotate(0 50 50)">
|
||||
<rect x="47" y="6.5" rx="3" ry="4.41" width="6" height="21" fill="#6ad7e5">
|
||||
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.9166666666666666s" repeatCount="indefinite"></animate>
|
||||
</rect>
|
||||
</g><g transform="rotate(30 50 50)">
|
||||
<rect x="47" y="6.5" rx="3" ry="4.41" width="6" height="21" fill="#6ad7e5">
|
||||
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.8333333333333334s" repeatCount="indefinite"></animate>
|
||||
</rect>
|
||||
</g><g transform="rotate(60 50 50)">
|
||||
<rect x="47" y="6.5" rx="3" ry="4.41" width="6" height="21" fill="#6ad7e5">
|
||||
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.75s" repeatCount="indefinite"></animate>
|
||||
</rect>
|
||||
</g><g transform="rotate(90 50 50)">
|
||||
<rect x="47" y="6.5" rx="3" ry="4.41" width="6" height="21" fill="#6ad7e5">
|
||||
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.6666666666666666s" repeatCount="indefinite"></animate>
|
||||
</rect>
|
||||
</g><g transform="rotate(120 50 50)">
|
||||
<rect x="47" y="6.5" rx="3" ry="4.41" width="6" height="21" fill="#6ad7e5">
|
||||
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.5833333333333334s" repeatCount="indefinite"></animate>
|
||||
</rect>
|
||||
</g><g transform="rotate(150 50 50)">
|
||||
<rect x="47" y="6.5" rx="3" ry="4.41" width="6" height="21" fill="#6ad7e5">
|
||||
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.5s" repeatCount="indefinite"></animate>
|
||||
</rect>
|
||||
</g><g transform="rotate(180 50 50)">
|
||||
<rect x="47" y="6.5" rx="3" ry="4.41" width="6" height="21" fill="#6ad7e5">
|
||||
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.4166666666666667s" repeatCount="indefinite"></animate>
|
||||
</rect>
|
||||
</g><g transform="rotate(210 50 50)">
|
||||
<rect x="47" y="6.5" rx="3" ry="4.41" width="6" height="21" fill="#6ad7e5">
|
||||
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.3333333333333333s" repeatCount="indefinite"></animate>
|
||||
</rect>
|
||||
</g><g transform="rotate(240 50 50)">
|
||||
<rect x="47" y="6.5" rx="3" ry="4.41" width="6" height="21" fill="#6ad7e5">
|
||||
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.25s" repeatCount="indefinite"></animate>
|
||||
</rect>
|
||||
</g><g transform="rotate(270 50 50)">
|
||||
<rect x="47" y="6.5" rx="3" ry="4.41" width="6" height="21" fill="#6ad7e5">
|
||||
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.16666666666666666s" repeatCount="indefinite"></animate>
|
||||
</rect>
|
||||
</g><g transform="rotate(300 50 50)">
|
||||
<rect x="47" y="6.5" rx="3" ry="4.41" width="6" height="21" fill="#6ad7e5">
|
||||
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.08333333333333333s" repeatCount="indefinite"></animate>
|
||||
</rect>
|
||||
</g><g transform="rotate(330 50 50)">
|
||||
<rect x="47" y="6.5" rx="3" ry="4.41" width="6" height="21" fill="#6ad7e5">
|
||||
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="0s" repeatCount="indefinite"></animate>
|
||||
</rect>
|
||||
</g>
|
||||
<!-- [ldio] generated by https://loading.io/ --></svg>
|
Before Width: | Height: | Size: 3.4 KiB |
Binary file not shown.
Before Width: | Height: | Size: 5.2 KiB |
Binary file not shown.
Before Width: | Height: | Size: 9.4 KiB |
@ -1,25 +0,0 @@
|
||||
{
|
||||
"short_name": "GoScrobble",
|
||||
"name": "GoScrobble - Go based scrobbler",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
},
|
||||
{
|
||||
"src": "logo192.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192"
|
||||
},
|
||||
{
|
||||
"src": "logo512.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
@ -1,368 +0,0 @@
|
||||
import axios from 'axios';
|
||||
import jwt from 'jwt-decode'
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
function getHeaders() {
|
||||
const user = JSON.parse(localStorage.getItem('user'));
|
||||
|
||||
if (user && user.jwt) {
|
||||
var unixtime = Math.round((new Date()).getTime() / 1000);
|
||||
if (user.exp < unixtime) {
|
||||
// Trigger refresh
|
||||
localStorage.removeItem('user');
|
||||
window.location.reload();
|
||||
// toast.warning("Session expired. Please log in again")
|
||||
// window.location.reload();
|
||||
return {};
|
||||
}
|
||||
|
||||
return { Authorization: 'Bearer ' + user.jwt };
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
function getUserUuid() {
|
||||
// TODO: move this to use Context values instead.
|
||||
const user = JSON.parse(localStorage.getItem('user'));
|
||||
|
||||
if (user && user.uuid) {
|
||||
return user.uuid
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function handleErrorResp(error) {
|
||||
if (error.response) {
|
||||
if (error.response.status === 401) {
|
||||
toast.error('Unauthorized')
|
||||
} else if (error.response.status === 429) {
|
||||
toast.error('Rate limited. Please try again shortly')
|
||||
} else {
|
||||
toast.error('An unknown error has occurred');
|
||||
}
|
||||
} else {
|
||||
toast.error('Failed to connect to API');
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
export const PostLogin = (formValues) => {
|
||||
return axios.post(process.env.REACT_APP_API_URL + "/api/v1/login", formValues)
|
||||
.then((response) => {
|
||||
if (response.data.token) {
|
||||
let expandedUser = jwt(response.data.token)
|
||||
let user = {
|
||||
jwt: response.data.token,
|
||||
uuid: expandedUser.sub,
|
||||
exp: expandedUser.exp,
|
||||
username: expandedUser.username,
|
||||
admin: expandedUser.admin,
|
||||
mod: expandedUser.mod,
|
||||
refresh_token: expandedUser.refresh_token,
|
||||
refresh_exp: expandedUser.refresh_exp,
|
||||
}
|
||||
|
||||
toast.success('Successfully logged in.');
|
||||
return user;
|
||||
} else {
|
||||
toast.error(response.data.error ? response.data.error: 'An Unknown Error has occurred');
|
||||
return null
|
||||
}
|
||||
}).catch((error) => {
|
||||
if (error.response === 401) {
|
||||
toast.error('Unauthorized')
|
||||
} else if (error.response === 429) {
|
||||
toast.error('Rate limited. Please try again shortly')
|
||||
} else {
|
||||
toast.error('Failed to connect');
|
||||
}
|
||||
return Promise.resolve();
|
||||
});
|
||||
};
|
||||
|
||||
export const PostRefreshToken = (refreshToken) => {
|
||||
return axios.post(process.env.REACT_APP_API_URL + "/api/v1/refresh", {token: refreshToken})
|
||||
.then((response) => {
|
||||
if (response.data.token) {
|
||||
let expandedUser = jwt(response.data.token)
|
||||
let user = {
|
||||
jwt: response.data.token,
|
||||
uuid: expandedUser.sub,
|
||||
exp: expandedUser.exp,
|
||||
username: expandedUser.username,
|
||||
admin: expandedUser.admin,
|
||||
mod: expandedUser.mod,
|
||||
refresh_token: expandedUser.refresh_token,
|
||||
refresh_exp: expandedUser.refresh_exp,
|
||||
}
|
||||
|
||||
return user;
|
||||
} else {
|
||||
toast.error(response.data.error ? response.data.error: 'An Unknown Error has occurred');
|
||||
return null
|
||||
}
|
||||
}).catch((error) => {
|
||||
if (error.response === 401) {
|
||||
toast.error('Unauthorized')
|
||||
} else if (error.response === 429) {
|
||||
toast.error('Rate limited. Please try again shortly')
|
||||
} else {
|
||||
toast.error('Failed to connect');
|
||||
}
|
||||
return Promise.resolve();
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
export const PostRegister = (formValues) => {
|
||||
return axios.post(process.env.REACT_APP_API_URL + "/api/v1/register", formValues)
|
||||
.then((response) => {
|
||||
if (response.data.message) {
|
||||
toast.success(response.data.message);
|
||||
|
||||
return Promise.resolve();
|
||||
} else {
|
||||
toast.error(response.data.error ? response.data.error: 'An Unknown Error has occurred');
|
||||
|
||||
return Promise.reject();
|
||||
}
|
||||
}).catch((error) => {
|
||||
handleErrorResp(error)
|
||||
return Promise.resolve();
|
||||
});
|
||||
};
|
||||
|
||||
export const PostResetPassword = (formValues) => {
|
||||
return axios.post(process.env.REACT_APP_API_URL + "/api/v1/resetpassword", formValues)
|
||||
.then((response) => {
|
||||
if (response.data.message) {
|
||||
toast.success(response.data.message);
|
||||
|
||||
return Promise.resolve();
|
||||
} else {
|
||||
toast.error(response.data.error ? response.data.error: 'An Unknown Error has occurred');
|
||||
|
||||
return Promise.reject();
|
||||
}
|
||||
}).catch((error) => {
|
||||
handleErrorResp(error)
|
||||
return Promise.resolve();
|
||||
});
|
||||
};
|
||||
|
||||
export const sendPasswordReset = (values) => {
|
||||
return axios.post(process.env.REACT_APP_API_URL + "/api/v1/sendreset", values).then(
|
||||
(data) => {
|
||||
return data.data;
|
||||
}).catch((error) => {
|
||||
return handleErrorResp(error)
|
||||
});
|
||||
};
|
||||
|
||||
export const getStats = () => {
|
||||
return axios.get(process.env.REACT_APP_API_URL + "/api/v1/stats").then(
|
||||
(data) => {
|
||||
return data.data;
|
||||
}).catch((error) => {
|
||||
return handleErrorResp(error)
|
||||
});
|
||||
};
|
||||
|
||||
export const getRecentScrobbles = (id) => {
|
||||
return axios.get(process.env.REACT_APP_API_URL + "/api/v1/user/" + id + "/scrobbles", { headers: getHeaders() })
|
||||
.then((data) => {
|
||||
return data.data;
|
||||
}).catch((error) => {
|
||||
return handleErrorResp(error)
|
||||
});
|
||||
};
|
||||
|
||||
export const getConfigs = () => {
|
||||
return axios.get(process.env.REACT_APP_API_URL + "/api/v1/config", { headers: getHeaders() })
|
||||
.then((data) => {
|
||||
return data.data;
|
||||
}).catch((error) => {
|
||||
return handleErrorResp(error)
|
||||
});
|
||||
};
|
||||
|
||||
export const postConfigs = (values, toggle) => {
|
||||
if (toggle) {
|
||||
values.REGISTRATION_ENABLED = "1"
|
||||
} else {
|
||||
values.REGISTRATION_ENABLED = "0"
|
||||
}
|
||||
|
||||
return axios.post(process.env.REACT_APP_API_URL + "/api/v1/config", values, { headers: getHeaders() })
|
||||
.then((data) => {
|
||||
if (data.data && data.data.message) {
|
||||
toast.success(data.data.message);
|
||||
} else if (data.data && data.data.error) {
|
||||
toast.error(data.data.error);
|
||||
}
|
||||
}).catch((error) => {
|
||||
return handleErrorResp(error)
|
||||
});
|
||||
};
|
||||
|
||||
export const getProfile = (userName) => {
|
||||
return axios.get(process.env.REACT_APP_API_URL + "/api/v1/profile/" + userName, { headers: getHeaders() })
|
||||
.then((data) => {
|
||||
return data.data;
|
||||
}).catch((error) => {
|
||||
return handleErrorResp(error)
|
||||
});
|
||||
};
|
||||
|
||||
export const getUser = () => {
|
||||
return axios.get(process.env.REACT_APP_API_URL + "/api/v1/user", { headers: getHeaders() })
|
||||
.then((data) => {
|
||||
return data.data;
|
||||
}).catch((error) => {
|
||||
return handleErrorResp(error)
|
||||
});
|
||||
};
|
||||
|
||||
export const patchUser = (values) => {
|
||||
return axios.patch(process.env.REACT_APP_API_URL + "/api/v1/user", values, { headers: getHeaders() })
|
||||
.then((data) => {
|
||||
return data.data;
|
||||
}).catch((error) => {
|
||||
return handleErrorResp(error)
|
||||
});
|
||||
};
|
||||
|
||||
export const validateResetPassword = (tokenStr) => {
|
||||
return axios.get(process.env.REACT_APP_API_URL + "/api/v1/user/", { headers: getHeaders() })
|
||||
.then((data) => {
|
||||
return data.data;
|
||||
}).catch((error) => {
|
||||
return handleErrorResp(error)
|
||||
});
|
||||
};
|
||||
|
||||
export const getSpotifyClientId = () => {
|
||||
return axios.get(process.env.REACT_APP_API_URL + "/api/v1/user/spotify", { headers: getHeaders() })
|
||||
.then((data) => {
|
||||
return data.data
|
||||
}).catch((error) => {
|
||||
return handleErrorResp(error)
|
||||
});
|
||||
}
|
||||
|
||||
export const spotifyConnectionRequest = () => {
|
||||
return getSpotifyClientId().then((resp) => {
|
||||
var scopes = 'user-read-recently-played user-read-currently-playing';
|
||||
|
||||
// Local, lets forward it to API
|
||||
let redirectUri = window.location.origin.toString()+ "/api/v1/link/spotify";
|
||||
|
||||
// Stupid dev
|
||||
if (window.location.origin.toString() === "http://localhost:3000") {
|
||||
redirectUri = "http://localhost:42069/api/v1/link/spotify"
|
||||
}
|
||||
|
||||
window.location = 'https://accounts.spotify.com/authorize' +
|
||||
'?response_type=code' +
|
||||
'&client_id=' + resp.token +
|
||||
'&scope=' + encodeURIComponent(scopes) +
|
||||
'&redirect_uri=' + encodeURIComponent(redirectUri) +
|
||||
'&state=' + getUserUuid();
|
||||
})
|
||||
};
|
||||
|
||||
export const spotifyDisonnectionRequest = () => {
|
||||
return axios.delete(process.env.REACT_APP_API_URL + "/api/v1/user/spotify", { headers: getHeaders() })
|
||||
.then((data) => {
|
||||
toast.success(data.data.message);
|
||||
return true
|
||||
}).catch((error) => {
|
||||
return handleErrorResp(error)
|
||||
});
|
||||
}
|
||||
|
||||
export const navidromeConnectionRequest = (values) => {
|
||||
return axios.post(process.env.REACT_APP_API_URL + "/api/v1/user/navidrome", values, { headers: getHeaders() })
|
||||
.then((data) => {
|
||||
toast.success(data.data.message);
|
||||
return true
|
||||
}).catch((error) => {
|
||||
return handleErrorResp(error)
|
||||
});
|
||||
};
|
||||
|
||||
export const navidromeDisonnectionRequest = () => {
|
||||
return axios.delete(process.env.REACT_APP_API_URL + "/api/v1/user/navidrome", { headers: getHeaders() })
|
||||
.then((data) => {
|
||||
toast.success(data.data.message);
|
||||
return true
|
||||
}).catch((error) => {
|
||||
return handleErrorResp(error)
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
export const getServerInfo = () => {
|
||||
return axios.get(process.env.REACT_APP_API_URL + "/api/v1/serverinfo")
|
||||
.then((data) => {
|
||||
return data.data
|
||||
}).catch((error) => {
|
||||
return handleErrorResp(error)
|
||||
});
|
||||
}
|
||||
|
||||
export const getArtist = (uuid) => {
|
||||
return axios.get(process.env.REACT_APP_API_URL + "/api/v1/artists/" + uuid).then(
|
||||
(data) => {
|
||||
return data.data;
|
||||
}).catch((error) => {
|
||||
return handleErrorResp(error)
|
||||
});
|
||||
};
|
||||
|
||||
export const getAlbum = (uuid) => {
|
||||
return axios.get(process.env.REACT_APP_API_URL + "/api/v1/albums/" + uuid).then(
|
||||
(data) => {
|
||||
return data.data;
|
||||
}).catch((error) => {
|
||||
return handleErrorResp(error)
|
||||
});
|
||||
};
|
||||
|
||||
export const getTrack = (uuid) => {
|
||||
return axios.get(process.env.REACT_APP_API_URL + "/api/v1/tracks/" + uuid).then(
|
||||
(data) => {
|
||||
return data.data;
|
||||
}).catch((error) => {
|
||||
return handleErrorResp(error)
|
||||
});
|
||||
};
|
||||
|
||||
export const getTopTracks = (uuid) => {
|
||||
return axios.get(process.env.REACT_APP_API_URL + "/api/v1/tracks/top/" + uuid).then(
|
||||
(data) => {
|
||||
return data.data;
|
||||
}).catch((error) => {
|
||||
return handleErrorResp(error)
|
||||
});
|
||||
}
|
||||
|
||||
export const getTopArtists = (uuid) => {
|
||||
return axios.get(process.env.REACT_APP_API_URL + "/api/v1/artists/top/" + uuid).then(
|
||||
(data) => {
|
||||
return data.data;
|
||||
}).catch((error) => {
|
||||
return handleErrorResp(error)
|
||||
});
|
||||
}
|
||||
|
||||
export const getTopUsersForTrack = (uuid) => {
|
||||
return axios.get(process.env.REACT_APP_API_URL + "/api/v1/tracks/" + uuid + "/top").then(
|
||||
(data) => {
|
||||
return data.data;
|
||||
}).catch((error) => {
|
||||
return handleErrorResp(error)
|
||||
});
|
||||
}
|
@ -1,68 +0,0 @@
|
||||
html, body {
|
||||
background-color: #282c34;
|
||||
/** WHY DOES THIS DEFAULT TO 1.5 */
|
||||
line-height: 1.3!important;
|
||||
}
|
||||
|
||||
.App {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.App-logo {
|
||||
height: 40vmin;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.App-logo {
|
||||
animation: App-logo-spin infinite 0.5s linear;
|
||||
}
|
||||
}
|
||||
|
||||
.toastNotifs {
|
||||
margin-top: 100px;
|
||||
z-index: 99999!important;
|
||||
}
|
||||
|
||||
.App-header {
|
||||
background-color: #282c34;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: calc(10px + 2vmin);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.pageWrapper {
|
||||
background-color: #282c34;
|
||||
padding: 90px 15px 0 15px;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
font-size: calc(10px + 2vmin);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.pageBody {
|
||||
padding: 20px 5px 5px 5px;
|
||||
font-size: 16pt;
|
||||
}
|
||||
|
||||
.App-link {
|
||||
color: #61dafb;
|
||||
}
|
||||
|
||||
@keyframes App-logo-spin {
|
||||
0% {
|
||||
transform: scale(1,1);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.05,1.05);
|
||||
}
|
||||
100% {
|
||||
transform: scale(1,1);
|
||||
}
|
||||
}
|
@ -1,57 +0,0 @@
|
||||
import { Route, Switch, withRouter } from 'react-router-dom';
|
||||
import Home from './Pages/Home';
|
||||
import About from './Pages/About';
|
||||
import Profile from './Pages/Profile';
|
||||
import Artist from './Pages/Artist';
|
||||
import Album from './Pages/Album';
|
||||
import Track from './Pages/Track';
|
||||
import TrackEdit from './Pages/TrackEdit';
|
||||
import User from './Pages/User';
|
||||
import Admin from './Pages/Admin';
|
||||
import Login from './Pages/Login';
|
||||
import Register from './Pages/Register';
|
||||
import Reset from './Pages/Reset';
|
||||
|
||||
import Navigation from './Components/Navigation';
|
||||
|
||||
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||
import './App.css';
|
||||
|
||||
const App = () => {
|
||||
let boolTrue = true;
|
||||
|
||||
// Remove loading spinner on load
|
||||
const el = document.querySelector(".loader-container");
|
||||
if (el) {
|
||||
el.remove();
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Navigation />
|
||||
<Switch>
|
||||
<Route exact={boolTrue} path={["/", "/home"]} component={Home} />
|
||||
<Route path="/about" component={About} />
|
||||
|
||||
<Route path="/user" component={User} />
|
||||
<Route path="/u/:uuid" component={Profile} />
|
||||
<Route path="/artist/:uuid" component={Artist} />
|
||||
<Route path="/album/:uuid" component={Album} />
|
||||
<Route path="/track/:uuid/edit" component={TrackEdit} />
|
||||
<Route path="/track/:uuid" component={Track} />
|
||||
|
||||
<Route path="/admin" component={Admin} />
|
||||
|
||||
<Route path="/login" component={Login} />
|
||||
<Route path="/register" component={Register} />
|
||||
|
||||
<Route path="/reset/:token" component={Reset} />
|
||||
<Route path="/reset" component={Reset} />
|
||||
|
||||
</Switch>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
export default withRouter(App);
|
@ -1,8 +0,0 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import App from './App';
|
||||
|
||||
test('renders learn react link', () => {
|
||||
render(<App />);
|
||||
const linkElement = screen.getByText(/learn react/i);
|
||||
expect(linkElement).toBeInTheDocument();
|
||||
});
|
@ -1,15 +0,0 @@
|
||||
.homeBanner {
|
||||
margin-top: 30px;
|
||||
width: 100%;
|
||||
max-width: 1100px;
|
||||
}
|
||||
|
||||
.homeBannerItem {
|
||||
float: left;
|
||||
text-align: center;
|
||||
width: 25%;
|
||||
}
|
||||
|
||||
.homeBannerItemCount {
|
||||
font-size: 1.9rem;
|
||||
}
|
@ -1,55 +0,0 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import '../App.css';
|
||||
import './HomeBanner.css';
|
||||
import { getStats } from '../Api/index';
|
||||
import ClipLoader from 'react-spinners/ClipLoader'
|
||||
|
||||
const HomeBanner = () => {
|
||||
let [bannerData, setBannerData] = useState({});
|
||||
let [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
getStats()
|
||||
.then(data => {
|
||||
if (data.users) {
|
||||
setBannerData(data);
|
||||
setIsLoading(false);
|
||||
}
|
||||
})
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="homeBanner">
|
||||
<div className="homeBannerItem">
|
||||
{isLoading
|
||||
? <ClipLoader color="#6AD7E5" size={34} />
|
||||
: <span className="homeBannerItemCount">{bannerData.scrobbles}</span>
|
||||
}
|
||||
<br/>Scrobbles
|
||||
</div>
|
||||
<div className="homeBannerItem">
|
||||
{isLoading
|
||||
? <ClipLoader color="#6AD7E5" size={34} />
|
||||
: <span className="homeBannerItemCount">{bannerData.users}</span>
|
||||
}
|
||||
<br/>Users
|
||||
</div>
|
||||
<div className="homeBannerItem">
|
||||
{isLoading
|
||||
? <ClipLoader color="#6AD7E5" size={34} />
|
||||
: <span className="homeBannerItemCount">{bannerData.tracks}</span>
|
||||
}
|
||||
<br/>Tracks
|
||||
</div>
|
||||
<div className="homeBannerItem">
|
||||
{isLoading
|
||||
? <ClipLoader color="#6AD7E5" size={34} />
|
||||
: <span className="homeBannerItemCount">{bannerData.artists}</span>
|
||||
}
|
||||
<br/>Artists
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default HomeBanner;
|
@ -1,35 +0,0 @@
|
||||
.navLink {
|
||||
padding: 0 15px 0 15px;
|
||||
color: #CCCCCC;
|
||||
}
|
||||
|
||||
.navLinkMobile {
|
||||
color: #CCCCCC;
|
||||
}
|
||||
|
||||
.navLink:hover {
|
||||
color: #666666;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.navLinkMobile:hover {
|
||||
color: #666666;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.navLinkLogin {
|
||||
margin-left: 15px;
|
||||
padding-left: 15px;
|
||||
border-left: 1px solid #282c34;
|
||||
}
|
||||
|
||||
.navLinkLoginMobile {
|
||||
margin: 0 auto 0 auto;
|
||||
padding: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.nav-logo {
|
||||
height: 50px;
|
||||
margin: -15px 5px -15px -5px;
|
||||
}
|
@ -1,181 +0,0 @@
|
||||
import { React, useState, useContext } from 'react';
|
||||
import { Navbar, NavbarBrand, Collapse, Nav, NavbarToggler, NavItem } from 'reactstrap';
|
||||
import { Link, useLocation } from 'react-router-dom';
|
||||
import logo from '../logo.png';
|
||||
import './Navigation.css';
|
||||
|
||||
import AuthContext from '../Contexts/AuthContext';
|
||||
|
||||
const menuItems = [
|
||||
'Home',
|
||||
// 'About',
|
||||
];
|
||||
|
||||
const loggedInMenuItems = [
|
||||
'Home',
|
||||
'My Profile',
|
||||
// 'Docs',
|
||||
]
|
||||
|
||||
const isMobile = () => {
|
||||
return (/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent))
|
||||
};
|
||||
|
||||
const Navigation = () => {
|
||||
const location = useLocation();
|
||||
|
||||
// Lovely hack to highlight the current page (:
|
||||
let active = "home"
|
||||
if (location && location.pathname && location.pathname.length > 1) {
|
||||
active = location.pathname.replace(/\//, "");
|
||||
}
|
||||
|
||||
let activeStyle = { color: '#FFFFFF' };
|
||||
let { user, Logout } = useContext(AuthContext);
|
||||
let [collapsed, setCollapsed] = useState(true);
|
||||
|
||||
const toggleCollapsed = () => {
|
||||
setCollapsed(!collapsed)
|
||||
}
|
||||
|
||||
const renderMobileNav = () => {
|
||||
return <Navbar color="dark" dark fixed="top">
|
||||
<NavbarBrand className="mr-auto"><img src={logo} className="nav-logo" alt="logo" /> GoScrobble</NavbarBrand>
|
||||
<NavbarToggler onClick={toggleCollapsed} className="mr-2" />
|
||||
<Collapse isOpen={!collapsed} navbar>
|
||||
{user ?
|
||||
<Nav className="navLinkLoginMobile" navbar>
|
||||
{loggedInMenuItems.map(menuItem =>
|
||||
<NavItem key={menuItem}>
|
||||
<Link
|
||||
key={menuItem}
|
||||
className="navLinkMobile"
|
||||
style={active === menuItem.toLowerCase() ? activeStyle : {}}
|
||||
to={menuItem === "My Profile" ? "/u/" + user.username : "/" + menuItem.toLowerCase()} onClick={toggleCollapsed}
|
||||
>{menuItem}</Link>
|
||||
</NavItem>
|
||||
)}
|
||||
<Link
|
||||
to="/user"
|
||||
style={active === "user" ? activeStyle : {}}
|
||||
className="navLinkMobile"
|
||||
onClick={toggleCollapsed}
|
||||
>Settings</Link>
|
||||
{user.admin &&
|
||||
<Link
|
||||
to="/admin"
|
||||
style={active === "admin" ? activeStyle : {}}
|
||||
className="navLink"
|
||||
onClick={toggleCollapsed}
|
||||
>Admin</Link>}
|
||||
<Link to="/" className="navLink" onClick={Logout}>Logout</Link>
|
||||
</Nav>
|
||||
: <Nav className="navLinkLoginMobile" navbar>
|
||||
{menuItems.map(menuItem =>
|
||||
<NavItem key={menuItem}>
|
||||
<Link
|
||||
key={menuItem}
|
||||
className="navLinkMobile"
|
||||
style={active === "home" && menuItem.toLowerCase() === "home" ? activeStyle : (active === menuItem.toLowerCase() ? activeStyle : {})}
|
||||
to={menuItem.toLowerCase() === "home" ? "/" : "/" + menuItem.toLowerCase()}
|
||||
onClick={toggleCollapsed}
|
||||
>{menuItem}
|
||||
</Link>
|
||||
</NavItem>
|
||||
)}
|
||||
<NavItem>
|
||||
<Link
|
||||
to="/login"
|
||||
style={active === "login" ? activeStyle : {}}
|
||||
className="navLinkMobile"
|
||||
onClick={toggleCollapsed}
|
||||
>Login</Link>
|
||||
</NavItem>
|
||||
<NavItem>
|
||||
<Link
|
||||
to="/register"
|
||||
className="navLinkMobile"
|
||||
style={active === "register" ? activeStyle : {}}
|
||||
onClick={toggleCollapsed}
|
||||
>Register</Link>
|
||||
</NavItem>
|
||||
</Nav>
|
||||
}
|
||||
</Collapse>
|
||||
</Navbar>
|
||||
}
|
||||
|
||||
const renderDesktopNav = () => {
|
||||
return <Navbar color="dark" dark fixed="top">
|
||||
<NavbarBrand className="mr-auto"><img src={logo} className="nav-logo" alt="logo" /> GoScrobble</NavbarBrand>
|
||||
{user ?
|
||||
<div>
|
||||
{loggedInMenuItems.map(menuItem =>
|
||||
<Link
|
||||
key={menuItem}
|
||||
className="navLink"
|
||||
style={active === menuItem.toLowerCase() ? activeStyle : {}}
|
||||
to={menuItem === "My Profile" ? "/u/" + user.username : "/" + menuItem.toLowerCase()}
|
||||
>
|
||||
{menuItem}
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
: <div>
|
||||
{menuItems.map(menuItem =>
|
||||
<Link
|
||||
key={menuItem}
|
||||
className="navLink"
|
||||
style={active === "home" && menuItem.toLowerCase() === "home" ? activeStyle : (active === menuItem.toLowerCase() ? activeStyle : {})}
|
||||
to={menuItem.toLowerCase() === "home" ? "/" : "/" + menuItem.toLowerCase()}
|
||||
>
|
||||
{menuItem}
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
{user ?
|
||||
<div className="navLinkLogin">
|
||||
<Link
|
||||
to="/user"
|
||||
style={active === "user" ? activeStyle : {}}
|
||||
className="navLink"
|
||||
>Settings</Link>
|
||||
{user.admin &&
|
||||
<Link
|
||||
to="/admin"
|
||||
style={active === "admin" ? activeStyle : {}}
|
||||
className="navLink"
|
||||
>Admin</Link>}
|
||||
<Link to="/admin" className="navLink" onClick={Logout}>Logout</Link>
|
||||
</div>
|
||||
:
|
||||
<div className="navLinkLogin">
|
||||
<Link
|
||||
to="/login"
|
||||
style={active === "login" ? activeStyle : {}}
|
||||
className="navLink"
|
||||
>Login</Link>
|
||||
<Link
|
||||
to="/register"
|
||||
className="navLink"
|
||||
style={active === "register" ? activeStyle : {}}
|
||||
>Register</Link>
|
||||
</div>
|
||||
|
||||
}
|
||||
</Navbar>
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{
|
||||
isMobile()
|
||||
? renderMobileNav()
|
||||
: renderDesktopNav()
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Navigation;
|
@ -1,36 +0,0 @@
|
||||
import React from "react";
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
const ScrobbleTable = (props) => {
|
||||
return (
|
||||
<div style={{
|
||||
border: `1px solid #FFFFFF`,
|
||||
width: `100%`,
|
||||
display: `flex`,
|
||||
flexWrap: `wrap`,
|
||||
minWidth: `300px`,
|
||||
maxWidth: `900px`,
|
||||
}}>
|
||||
{
|
||||
props.data &&
|
||||
props.data.map(function (element) {
|
||||
let localTime = new Date(element.time);
|
||||
return <div style={{borderBottom: `1px solid #CCC`, width: `100%`, padding: `2px`}} key={"box" + element.time}>
|
||||
{localTime.toLocaleString()}<br/>
|
||||
<Link
|
||||
key={"artist" + element.time}
|
||||
to={"/artist/"+element.artist.uuid}
|
||||
>{element.artist.name}</Link> -
|
||||
<Link
|
||||
key={"track" + element.time}
|
||||
to={"/track/"+element.track.uuid}
|
||||
> {element.track.name}</Link>
|
||||
</div>;
|
||||
|
||||
})
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ScrobbleTable;
|
@ -1,13 +0,0 @@
|
||||
.biggestWrapper {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.biggestBox {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 300px;
|
||||
height: 300px;
|
||||
}
|
@ -1,131 +0,0 @@
|
||||
import React from "react";
|
||||
import './TopTable.css'
|
||||
import TopTableBox from './TopTableBox';
|
||||
|
||||
const TopTable = (props) => {
|
||||
if (!props.items || Object.keys(props.items).length < 1) {
|
||||
return (
|
||||
<span>Not enough data to show top {props.type}s.<br/></span>
|
||||
)
|
||||
}
|
||||
|
||||
let tracks = props.items;
|
||||
|
||||
return (
|
||||
<div style={{textAlign: `center`}}>
|
||||
<span>Top {props.type}s</span>
|
||||
<div className="biggestWrapper">
|
||||
<div className="biggestBox">
|
||||
<TopTableBox
|
||||
size={300}
|
||||
number="1"
|
||||
title={tracks[1].name}
|
||||
link={"/" + props.type + "/" + tracks[1].uuid}
|
||||
uuid={tracks[1].img}
|
||||
/>
|
||||
</div>
|
||||
{ Object.keys(props.items).length > 5 &&
|
||||
<div className="biggestBox">
|
||||
<TopTableBox
|
||||
size={150}
|
||||
number="2"
|
||||
title={tracks[2].name}
|
||||
link={"/" + props.type + "/" + tracks[2].uuid}
|
||||
uuid={tracks[2].img}
|
||||
/>
|
||||
<TopTableBox
|
||||
size={150}
|
||||
number="3"
|
||||
title={tracks[3].name}
|
||||
link={"/" + props.type + "/" + tracks[3].uuid}
|
||||
uuid={tracks[3].img}
|
||||
/>
|
||||
<TopTableBox
|
||||
size={150}
|
||||
number="4"
|
||||
title={tracks[4].name}
|
||||
link={"/" + props.type + "/" + tracks[4].uuid}
|
||||
uuid={tracks[4].img}
|
||||
/>
|
||||
<TopTableBox
|
||||
size={150}
|
||||
number="5"
|
||||
title={tracks[5].name}
|
||||
link={"/" + props.type + "/" + tracks[5].uuid}
|
||||
uuid={tracks[5].img}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
{ Object.keys(props.items).length >= 14 &&
|
||||
<div className="biggestBox">
|
||||
<TopTableBox
|
||||
size={100}
|
||||
number="6"
|
||||
title={tracks[6].name}
|
||||
link={"/" + props.type + "/" + tracks[6].uuid}
|
||||
uuid={tracks[6].img}
|
||||
/>
|
||||
<TopTableBox
|
||||
size={100}
|
||||
number="7"
|
||||
title={tracks[7].name}
|
||||
link={"/" + props.type + "/" + tracks[7].uuid}
|
||||
uuid={tracks[7].img}
|
||||
/>
|
||||
<TopTableBox
|
||||
size={100}
|
||||
number="8"
|
||||
title={tracks[8].name}
|
||||
link={"/" + props.type + "/" + tracks[8].uuid}
|
||||
uuid={tracks[8].img}
|
||||
/>
|
||||
<TopTableBox
|
||||
size={100}
|
||||
number="9"
|
||||
title={tracks[9].name}
|
||||
link={"/" + props.type + "/" + tracks[9].uuid}
|
||||
uuid={tracks[9].img}
|
||||
/>
|
||||
<TopTableBox
|
||||
size={100}
|
||||
number="10"
|
||||
title={tracks[10].name}
|
||||
link={"/" + props.type + "/" + tracks[10].uuid}
|
||||
uuid={tracks[10].img}
|
||||
/>
|
||||
<TopTableBox
|
||||
size={100}
|
||||
number="11"
|
||||
title={tracks[11].name}
|
||||
link={"/" + props.type + "/" + tracks[11].uuid}
|
||||
uuid={tracks[11].img}
|
||||
/>
|
||||
<TopTableBox
|
||||
size={100}
|
||||
number="12"
|
||||
title={tracks[12].name}
|
||||
link={"/" + props.type + "/" + tracks[12].uuid}
|
||||
uuid={tracks[12].img}
|
||||
/>
|
||||
<TopTableBox
|
||||
size={100}
|
||||
number="13"
|
||||
title={tracks[13].name}
|
||||
link={"/" + props.type + "/" + tracks[13].uuid}
|
||||
uuid={tracks[13].img}
|
||||
/>
|
||||
<TopTableBox
|
||||
size={100}
|
||||
number="14"
|
||||
title={tracks[14].name}
|
||||
link={"/" + props.type + "/" + tracks[14].uuid}
|
||||
uuid={tracks[14].img}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default TopTable;
|
@ -1,22 +0,0 @@
|
||||
.topTableBox:hover {
|
||||
opacity: 0.7;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
img {
|
||||
background-size: cover;
|
||||
background-position: top center;
|
||||
}
|
||||
|
||||
.topOverlay {
|
||||
position: absolute;
|
||||
padding: 2px 5px 5px 5px;
|
||||
background-color: rgba(0, 0, 0, 0.6);
|
||||
line-height: 0.6em;
|
||||
}
|
||||
|
||||
.topText {
|
||||
margin: -2px 0 0 0;
|
||||
font-size: 11pt;
|
||||
color: #FFF;
|
||||
}
|
@ -1,29 +0,0 @@
|
||||
import React from "react";
|
||||
import { Link } from 'react-router-dom';
|
||||
import './TopTableBox.css'
|
||||
|
||||
const TopTableBox = (props) => {
|
||||
return (
|
||||
<Link to={props.link} float="left" >
|
||||
<div
|
||||
className="topTableBox"
|
||||
style={{
|
||||
backgroundImage: `url(${process.env.REACT_APP_API_URL + "/img/" + props.uuid + "_full.jpg"})`,
|
||||
backgroundSize: `cover`,
|
||||
backgroundPosition: `top center`,
|
||||
width: `${props.size}px`,
|
||||
height: `${props.size}px`,
|
||||
float: `left`,
|
||||
}} >
|
||||
<div className="topOverlay" style={{ maxWidth: `${props.size-'5'}px` }}>
|
||||
<span className="topText" style={{
|
||||
fontSize: `${props.size === 300 ? '11pt' : (props.size === 150 ? '8pt': '8pt' )}`
|
||||
}}>#{props.number} {props.title}</span>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
export default TopTableBox;
|
@ -1,56 +0,0 @@
|
||||
import { Link } from 'react-router-dom';
|
||||
import './TopUserTable.css'
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import ScaleLoader from 'react-spinners/ScaleLoader';
|
||||
import { getTopUsersForTrack } from '../Api/index'
|
||||
|
||||
const TopUserTable = (props) => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [data, setData] = useState({});
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (!props.uuid) {
|
||||
return false;
|
||||
}
|
||||
|
||||
getTopUsersForTrack(props.uuid)
|
||||
.then(data => {
|
||||
setData(data);
|
||||
setLoading(false);
|
||||
})
|
||||
}, [props.uuid])
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="pageWrapper">
|
||||
<ScaleLoader color="#6AD7E5" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
width: `100%`,
|
||||
display: `flex`,
|
||||
flexWrap: `wrap`,
|
||||
marginLeft: `20px`,
|
||||
textAlign: `left`,
|
||||
}}>
|
||||
{
|
||||
data.items &&
|
||||
data.items.map(function (element) {
|
||||
return <div style={{width: `100%`, padding: `2px`}} key={"box" + props.uuid}>
|
||||
<Link
|
||||
key={"user" + element.user_uuid}
|
||||
to={"/u/"+element.user_name}
|
||||
>{element.user_name}</Link> ({element.count})
|
||||
</div>;
|
||||
|
||||
})
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default TopUserTable;
|
@ -1,5 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
const AuthContext = React.createContext();
|
||||
|
||||
export default AuthContext;
|
@ -1,81 +0,0 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
import AuthContext from './AuthContext';
|
||||
import { PostLogin, PostRegister, PostResetPassword, PostRefreshToken } from '../Api/index';
|
||||
|
||||
const AuthContextProvider = ({ children }) => {
|
||||
const [user, setUser] = useState();
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true)
|
||||
let curTime = Math.round((new Date()).getTime() / 1000);
|
||||
let user = JSON.parse(localStorage.getItem('user'));
|
||||
|
||||
// Confirm JWT is set.
|
||||
if (user && user.jwt) {
|
||||
// Check refresh expiry is valid.
|
||||
if (user.refresh_exp && (user.refresh_exp > curTime)) {
|
||||
// Check if JWT is still valid
|
||||
if (user.exp < curTime) {
|
||||
// Refresh if not
|
||||
user = RefreshToken(user.refresh_token)
|
||||
localStorage.setItem('user', JSON.stringify(user));
|
||||
}
|
||||
|
||||
setUser(user);
|
||||
}
|
||||
}
|
||||
setLoading(false)
|
||||
}, []);
|
||||
|
||||
const Login = (formValues) => {
|
||||
setLoading(true);
|
||||
PostLogin(formValues).then(user => {
|
||||
if (user) {
|
||||
setUser(user);
|
||||
localStorage.setItem('user', JSON.stringify(user));
|
||||
}
|
||||
setLoading(false);
|
||||
})
|
||||
}
|
||||
|
||||
const Register = (formValues) => {
|
||||
setLoading(true);
|
||||
return PostRegister(formValues).then(response => {
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
const ResetPassword = (formValues) => {
|
||||
return PostResetPassword(formValues);
|
||||
}
|
||||
|
||||
const RefreshToken = (refreshToken) => {
|
||||
return PostRefreshToken(refreshToken);
|
||||
}
|
||||
|
||||
const Logout = () => {
|
||||
localStorage.removeItem("user");
|
||||
setUser(null)
|
||||
toast.success('Successfully logged out.');
|
||||
};
|
||||
|
||||
return (
|
||||
<AuthContext.Provider
|
||||
value={{
|
||||
Logout,
|
||||
Login,
|
||||
Register,
|
||||
ResetPassword,
|
||||
RefreshToken,
|
||||
loading,
|
||||
user,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export default AuthContextProvider;
|
@ -1,3 +0,0 @@
|
||||
import { createBrowserHistory } from "history";
|
||||
|
||||
export const history = createBrowserHistory();
|
@ -1 +0,0 @@
|
||||
|
@ -1,25 +0,0 @@
|
||||
import '../App.css';
|
||||
import './About.css';
|
||||
|
||||
const About = () => {
|
||||
return (
|
||||
<div className="pageWrapper">
|
||||
<h1>
|
||||
About GoScrobble.com
|
||||
</h1>
|
||||
<p className="aboutBody">
|
||||
Go-Scrobble is an open source music scrobbling service written in Go and React.<br/>
|
||||
Used to track your listening history and build a profile to discover new music.
|
||||
</p>
|
||||
<a
|
||||
className="pageBody"
|
||||
href="https://gitlab.com/idanoo/go-scrobble"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>gitlab.com/idanoo/go-scrobble
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default About;
|
@ -1,9 +0,0 @@
|
||||
.adminFields {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.admin {
|
||||
height: 50px;
|
||||
width: 100%;
|
||||
margin-top:-5px;
|
||||
}
|
@ -1,112 +0,0 @@
|
||||
import React, { useContext, useState, useEffect } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import '../App.css';
|
||||
import './Admin.css';
|
||||
import { Button } from 'reactstrap';
|
||||
import { Formik, Form, Field } from 'formik';
|
||||
import ScaleLoader from 'react-spinners/ScaleLoader';
|
||||
import AuthContext from '../Contexts/AuthContext';
|
||||
import { Switch } from 'formik-material-ui';
|
||||
import { getConfigs, postConfigs } from '../Api/index'
|
||||
|
||||
const Admin = () => {
|
||||
const history = useHistory();
|
||||
const { user } = useContext(AuthContext);
|
||||
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [configs, setConfigs] = useState({})
|
||||
const [toggle, setToggle] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
getConfigs()
|
||||
.then(data => {
|
||||
if (data.configs) {
|
||||
setConfigs(data.configs);
|
||||
setToggle(data.configs.REGISTRATION_ENABLED === "1")
|
||||
setLoading(false);
|
||||
}
|
||||
})
|
||||
}, [])
|
||||
|
||||
const handleToggle = () => {
|
||||
setToggle(!toggle);
|
||||
};
|
||||
|
||||
if (!user) {
|
||||
history.push("/login")
|
||||
}
|
||||
|
||||
if (user && !user.admin) {
|
||||
history.push("/Dashboard")
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="pageWrapper">
|
||||
<ScaleLoader color="#6AD7E5" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="pageWrapper">
|
||||
<h1>
|
||||
Admin Panel
|
||||
</h1>
|
||||
<div className="pageBody">
|
||||
<Formik
|
||||
initialValues={configs}
|
||||
onSubmit={(values) => postConfigs(values, toggle)}
|
||||
>
|
||||
<Form><br/>
|
||||
<label>
|
||||
<Field
|
||||
type="checkbox"
|
||||
name="REGISTRATION_ENABLED"
|
||||
onChange={handleToggle}
|
||||
component={Switch}
|
||||
checked={toggle}
|
||||
value={toggle}
|
||||
/>
|
||||
Registration Enabled
|
||||
</label><br/><br/>
|
||||
<label>
|
||||
LastFM Api Key<br/>
|
||||
<Field
|
||||
name="LASTFM_API_KEY"
|
||||
type="text"
|
||||
className="loginFields"
|
||||
/>
|
||||
</label>
|
||||
<br/>
|
||||
<label>
|
||||
Spotify App ID<br/>
|
||||
<Field
|
||||
name="SPOTIFY_APP_ID"
|
||||
type="text"
|
||||
className="loginFields"
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
Spotify App Secret<br/>
|
||||
<Field
|
||||
name="SPOTIFY_APP_SECRET"
|
||||
type="text"
|
||||
className="loginFields"
|
||||
/>
|
||||
</label>
|
||||
<br/><br/>
|
||||
<Button
|
||||
color="primary"
|
||||
type="submit"
|
||||
className="loginButton"
|
||||
disabled={loading}
|
||||
>Update</Button>
|
||||
</Form>
|
||||
</Formik>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Admin;
|
@ -1,60 +0,0 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import '../App.css';
|
||||
import './Album.css';
|
||||
import ScaleLoader from 'react-spinners/ScaleLoader';
|
||||
import { getAlbum } from '../Api/index'
|
||||
|
||||
const Album = (route) => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [album, setAlbum] = useState({});
|
||||
|
||||
let albumUUID = false;
|
||||
if (route && route.match && route.match.params && route.match.params.uuid) {
|
||||
albumUUID = route.match.params.uuid;
|
||||
} else {
|
||||
albumUUID = false;
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!albumUUID) {
|
||||
return false;
|
||||
}
|
||||
|
||||
getAlbum(albumUUID)
|
||||
.then(data => {
|
||||
setAlbum(data);
|
||||
setLoading(false);
|
||||
})
|
||||
}, [albumUUID])
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="pageWrapper">
|
||||
<ScaleLoader color="#6AD7E5" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (!albumUUID || !album) {
|
||||
return (
|
||||
<div className="pageWrapper">
|
||||
Unable to fetch user
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="pageWrapper">
|
||||
<h1>
|
||||
{album.name}
|
||||
</h1>
|
||||
<div className="pageBody">
|
||||
<img src={process.env.REACT_APP_API_URL + "/img/" + album.uuid + "_full.jpg"} alt={album.name} style={{maxWidth: `300px`, maxHeight: `300px`}}/><br/><br/>
|
||||
{album.mbid && <a rel="noreferrer" target="_blank" href={"https://musicbrainz.org/album/" + album.mbid}>Open on MusicBrainz<br/></a>}
|
||||
{album.spotify_id && <a rel="noreferrer" target="_blank" href={"https://open.spotify.com/album/" + album.spotify_id}>Open on Spotify<br/></a>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Album;
|
@ -1,60 +0,0 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import '../App.css';
|
||||
import './Artist.css';
|
||||
import ScaleLoader from 'react-spinners/ScaleLoader';
|
||||
import { getArtist } from '../Api/index'
|
||||
|
||||
const Artist = (route) => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [artist, setArtist] = useState({});
|
||||
|
||||
let artistUUID = false;
|
||||
if (route && route.match && route.match.params && route.match.params.uuid) {
|
||||
artistUUID = route.match.params.uuid;
|
||||
} else {
|
||||
artistUUID = false;
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!artistUUID) {
|
||||
return false;
|
||||
}
|
||||
|
||||
getArtist(artistUUID)
|
||||
.then(data => {
|
||||
setArtist(data);
|
||||
setLoading(false);
|
||||
})
|
||||
}, [artistUUID])
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="pageWrapper">
|
||||
<ScaleLoader color="#6AD7E5" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (!artistUUID || !artist) {
|
||||
return (
|
||||
<div className="pageWrapper">
|
||||
Unable to fetch user
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="pageWrapper">
|
||||
<h1>
|
||||
{artist.name}
|
||||
</h1>
|
||||
<div className="pageBody" style={{textAlign: `center`}}>
|
||||
<img src={process.env.REACT_APP_API_URL + "/img/" + artist.uuid + "_full.jpg"} alt={artist.name} style={{maxWidth: `300px`, maxHeight: `300px`}}/><br/><br/>
|
||||
{artist.mbid && <a rel="noreferrer" target="_blank" href={"https://musicbrainz.org/artist/" + artist.mbid}>Open on MusicBrainz<br/></a>}
|
||||
{artist.spotify_id && <a rel="noreferrer" target="_blank" href={"https://open.spotify.com/artist/" + artist.spotify_id}>Open on Spotify<br/></a>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Artist;
|
@ -1,11 +0,0 @@
|
||||
.homeText {
|
||||
margin: 0;
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.subHomeText {
|
||||
margin-top: -5px;
|
||||
font-style: italic;
|
||||
color: #CCC;
|
||||
font-size: 1.4rem;
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
import logo from '../logo.png';
|
||||
import '../App.css';
|
||||
import './Home.css';
|
||||
import HomeBanner from '../Components/HomeBanner';
|
||||
import React from 'react';
|
||||
|
||||
const Home = () => {
|
||||
return (
|
||||
<div className="pageWrapper">
|
||||
<img src={logo} className="App-logo" alt="logo" />
|
||||
<p className="homeText">GoScrobble is an open source music scrobbling service.</p>
|
||||
<p className="subHomeText">Supports Spotify, Jellyfin, Navidrome / Subsonic / Airsonic.</p>
|
||||
<HomeBanner />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Home;
|
@ -1,9 +0,0 @@
|
||||
.loginFields {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.loginButton {
|
||||
height: 50px;
|
||||
width: 100%;
|
||||
margin-top:-5px;
|
||||
}
|
@ -1,74 +0,0 @@
|
||||
import React, { useContext } from 'react';
|
||||
import '../App.css';
|
||||
import './Login.css';
|
||||
import { Button } from 'reactstrap';
|
||||
import { Formik, Form, Field } from 'formik';
|
||||
import ScaleLoader from 'react-spinners/ScaleLoader';
|
||||
import AuthContext from '../Contexts/AuthContext';
|
||||
import { useHistory } from "react-router";
|
||||
|
||||
const Login = () => {
|
||||
const history = useHistory();
|
||||
let boolTrue = true;
|
||||
let { Login, loading, user } = useContext(AuthContext);
|
||||
|
||||
if (user) {
|
||||
history.push("/u/" + user.username);
|
||||
}
|
||||
|
||||
const redirectReset = () => {
|
||||
history.push("/reset")
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="pageWrapper">
|
||||
<h1>
|
||||
Login
|
||||
</h1>
|
||||
<div className="pageBody">
|
||||
<Formik
|
||||
initialValues={{ username: '', password: '' }}
|
||||
onSubmit={values => Login(values)}
|
||||
>
|
||||
<Form>
|
||||
<label>
|
||||
Email / Username<br/>
|
||||
<Field
|
||||
name="username"
|
||||
type="text"
|
||||
required={boolTrue}
|
||||
className="loginFields"
|
||||
/>
|
||||
</label>
|
||||
<br/>
|
||||
<label>
|
||||
Password<br/>
|
||||
<Field
|
||||
name="password"
|
||||
type="password"
|
||||
className="loginFields"
|
||||
/>
|
||||
</label>
|
||||
<br/><br/>
|
||||
<Button
|
||||
color="primary"
|
||||
type="submit"
|
||||
className="loginButton"
|
||||
disabled={loading}
|
||||
>{loading ? <ScaleLoader color="#FFF" size={35} /> : "Login"}</Button>
|
||||
<br/><br/>
|
||||
<Button
|
||||
color="secondary"
|
||||
type="button"
|
||||
className="loginButton"
|
||||
onClick={redirectReset}
|
||||
disabled={loading}
|
||||
>Reset Password</Button>
|
||||
</Form>
|
||||
</Formik>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Login;
|
@ -1,81 +0,0 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import '../App.css';
|
||||
import './Profile.css';
|
||||
import ScaleLoader from 'react-spinners/ScaleLoader';
|
||||
import { getProfile, getTopTracks, getTopArtists } from '../Api/index'
|
||||
import ScrobbleTable from '../Components/ScrobbleTable'
|
||||
import TopTable from '../Components/TopTable'
|
||||
|
||||
const Profile = (route) => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [profile, setProfile] = useState({});
|
||||
const [topTracks, setTopTracks] = useState({})
|
||||
const [topArtists, setTopArtists] = useState({})
|
||||
|
||||
let username = false;
|
||||
if (route && route.match && route.match.params && route.match.params.uuid) {
|
||||
username = route.match.params.uuid;
|
||||
} else {
|
||||
username = false;
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!username) {
|
||||
return false;
|
||||
}
|
||||
|
||||
getProfile(username)
|
||||
.then(data => {
|
||||
setProfile(data);
|
||||
|
||||
// Fetch top tracks
|
||||
getTopTracks(data.uuid)
|
||||
.then(data => {
|
||||
setTopTracks(data.tracks)
|
||||
})
|
||||
|
||||
// Fetch top artists
|
||||
getTopArtists(data.uuid)
|
||||
.then(data => {
|
||||
setTopArtists(data.artists)
|
||||
})
|
||||
|
||||
setLoading(false);
|
||||
})
|
||||
|
||||
}, [username])
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="pageWrapper">
|
||||
<ScaleLoader color="#6AD7E5" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (!username || !profile.username) {
|
||||
return (
|
||||
<div className="pageWrapper">
|
||||
Unable to fetch user
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="pageWrapper">
|
||||
<h1>
|
||||
{profile.username}'s Profile
|
||||
</h1>
|
||||
<div className="pageBody">
|
||||
<TopTable type="track" items={topTracks} />
|
||||
<br/>
|
||||
<TopTable type="artist" items={topArtists} />
|
||||
<br/>
|
||||
Last 10 scrobbles<br/>
|
||||
<ScrobbleTable data={profile.scrobbles}/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Profile;
|
@ -1,9 +0,0 @@
|
||||
.registerFields {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.registerButton {
|
||||
height: 50px;
|
||||
width: 100%;
|
||||
margin-top:-5px;
|
||||
}
|
@ -1,111 +0,0 @@
|
||||
import React, { useContext, useState, useEffect } from 'react';
|
||||
import '../App.css';
|
||||
import './Register.css';
|
||||
import { Button } from 'reactstrap';
|
||||
import ScaleLoader from "react-spinners/ScaleLoader";
|
||||
import AuthContext from '../Contexts/AuthContext';
|
||||
import { Formik, Field, Form } from 'formik';
|
||||
import { useHistory } from 'react-router';
|
||||
import { getServerInfo } from '../Api/index';
|
||||
const Register = () => {
|
||||
const history = useHistory();
|
||||
let boolTrue = true;
|
||||
let { Register, user, loading } = useContext(AuthContext);
|
||||
let [serverInfo, setServerInfo] = useState({ registration_enabled: true });
|
||||
let [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (user) {
|
||||
return
|
||||
}
|
||||
getServerInfo()
|
||||
.then(data => {
|
||||
setServerInfo(data);
|
||||
setIsLoading(false);
|
||||
})
|
||||
}, [user])
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="pageWrapper">
|
||||
<ScaleLoader color="#6AD7E5" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (user) {
|
||||
history.push("/dashboard");
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="pageWrapper">
|
||||
{
|
||||
serverInfo.registration_enabled !== "1" ?
|
||||
<p>Registration is temporarily disabled. Please try again soon!</p>
|
||||
:
|
||||
<div>
|
||||
<h1>
|
||||
Register
|
||||
</h1>
|
||||
<div className="pageBody">
|
||||
<Formik
|
||||
initialValues={{ username: '', email: '', password: '', passwordconfirm: '' }}
|
||||
onSubmit={async values => Register(values)}
|
||||
>
|
||||
<Form>
|
||||
<label>
|
||||
Username<br/>
|
||||
<Field
|
||||
name="username"
|
||||
type="text"
|
||||
required={boolTrue}
|
||||
className="registerFields"
|
||||
/>
|
||||
</label>
|
||||
<br/>
|
||||
<label>
|
||||
Email<br/>
|
||||
<Field
|
||||
name="email"
|
||||
type="email"
|
||||
required={boolTrue}
|
||||
className="registerFields"
|
||||
/>
|
||||
</label>
|
||||
<br/>
|
||||
<label>
|
||||
Password<br/>
|
||||
<Field
|
||||
name="password"
|
||||
type="password"
|
||||
required={boolTrue}
|
||||
className="registerFields"
|
||||
/>
|
||||
</label>
|
||||
<br/>
|
||||
<label>
|
||||
Confirm Password<br/>
|
||||
<Field
|
||||
name="passwordconfirm"
|
||||
type="password"
|
||||
required={boolTrue}
|
||||
className="registerFields"
|
||||
/>
|
||||
</label>
|
||||
<br/><br/>
|
||||
<Button
|
||||
color="primary"
|
||||
type="submit"
|
||||
className="registerButton"
|
||||
disabled={loading}
|
||||
>{loading ? <ScaleLoader color="#FFF" size={35} /> : "Register"}</Button>
|
||||
</Form>
|
||||
</Formik>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Register;
|
@ -1,9 +0,0 @@
|
||||
.resetFields {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.resetButton {
|
||||
height: 50px;
|
||||
width: 100%;
|
||||
margin-top:-5px;
|
||||
}
|
@ -1,152 +0,0 @@
|
||||
import React, { useState, useEffect, useContext } from 'react';
|
||||
import '../App.css';
|
||||
import './Reset.css';
|
||||
import { Button } from 'reactstrap';
|
||||
import { Formik, Form, Field } from 'formik';
|
||||
import ScaleLoader from 'react-spinners/ScaleLoader';
|
||||
import { validateResetPassword, sendPasswordReset } from '../Api/index';
|
||||
import AuthContext from '../Contexts/AuthContext';
|
||||
|
||||
const Reset = (route) => {
|
||||
let boolTrue = true;
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [reset, setReset] = useState({});
|
||||
const [sent, setSent] = useState(false);
|
||||
|
||||
let { ResetPassword } = useContext(AuthContext);
|
||||
|
||||
let reqToken = false;
|
||||
if (route && route.match && route.match.params && route.match.params.token) {
|
||||
reqToken = route.match.params.token
|
||||
}
|
||||
|
||||
const sendReset = (values) => {
|
||||
sendPasswordReset(values).then(() => {
|
||||
setSent(true);
|
||||
});
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!reqToken) {
|
||||
setLoading(false);
|
||||
return false;
|
||||
}
|
||||
|
||||
validateResetPassword(reqToken)
|
||||
.then(data => {
|
||||
setReset(data);
|
||||
setLoading(false);
|
||||
})
|
||||
}, [reqToken])
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="pageWrapper">
|
||||
<ScaleLoader color="#6AD7E5" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (sent) {
|
||||
return (
|
||||
<div className="pageWrapper">
|
||||
<h1>
|
||||
Check your email!
|
||||
</h1>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (!reqToken) {
|
||||
return (
|
||||
<div className="pageWrapper">
|
||||
<h1>
|
||||
Reset Password
|
||||
</h1>
|
||||
<div className="pageBody">
|
||||
<Formik
|
||||
initialValues={{ email: '' }}
|
||||
onSubmit={values => sendReset(values)}
|
||||
>
|
||||
<Form>
|
||||
<label>
|
||||
Email<br/>
|
||||
<Field
|
||||
name="email"
|
||||
type="email"
|
||||
required={boolTrue}
|
||||
className="loginFields"
|
||||
/>
|
||||
</label>
|
||||
<br/><br/>
|
||||
<Button
|
||||
color="primary"
|
||||
type="submit"
|
||||
className="loginButton"
|
||||
disabled={loading}
|
||||
>{loading ? <ScaleLoader color="#FFF" size={35} /> : "Reset"}</Button>
|
||||
</Form>
|
||||
</Formik>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (reqToken && !reset.valid) {
|
||||
return (
|
||||
<div className="pageWrapper">
|
||||
Invalid Reset Token or Token expired
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="pageWrapper">
|
||||
<h1>
|
||||
Reset Password
|
||||
</h1>
|
||||
<div className="resetBody">
|
||||
<Formik
|
||||
initialValues={{ password: '', comfirmpassword: '', token: reqToken }}
|
||||
onSubmit={values => ResetPassword(values)}
|
||||
>
|
||||
<Form>
|
||||
<label>
|
||||
New Password<br/>
|
||||
<Field
|
||||
name="password"
|
||||
type="password"
|
||||
required={boolTrue}
|
||||
className="resetFields"
|
||||
/>
|
||||
</label>
|
||||
<br/>
|
||||
<label>
|
||||
Confirm New Password<br/>
|
||||
<Field
|
||||
name="comfirmpassword"
|
||||
type="password"
|
||||
required={boolTrue}
|
||||
className="resetFields"
|
||||
/>
|
||||
</label>
|
||||
<Field
|
||||
name="token"
|
||||
type="hidden"
|
||||
className="resetFields"
|
||||
/>
|
||||
<br/><br/>
|
||||
<Button
|
||||
color="primary"
|
||||
type="submit"
|
||||
className="loginButton"
|
||||
disabled={loading}
|
||||
>{loading ? <ScaleLoader color="#FFF" size={35} /> : "Reset"}</Button>
|
||||
</Form>
|
||||
</Formik>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Reset;
|
@ -1,20 +0,0 @@
|
||||
import React from 'react';
|
||||
import '../App.css';
|
||||
import './Settings.css';
|
||||
|
||||
const Settings = () => {
|
||||
return (
|
||||
<div className="pageWrapper">
|
||||
<h1>
|
||||
Settings
|
||||
</h1>
|
||||
<div className="pageBody">
|
||||
<p>
|
||||
All the settings
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Settings;
|
@ -1,115 +0,0 @@
|
||||
import React, { useContext, useState, useEffect } from 'react';
|
||||
import '../App.css';
|
||||
import './Track.css';
|
||||
import TopUserTable from '../Components/TopUserTable';
|
||||
import ScaleLoader from 'react-spinners/ScaleLoader';
|
||||
import { getTrack } from '../Api/index'
|
||||
import { Link } from 'react-router-dom';
|
||||
import AuthContext from '../Contexts/AuthContext';
|
||||
|
||||
const Track = (route) => {
|
||||
const { user } = useContext(AuthContext);
|
||||
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [track, setTrack] = useState({});
|
||||
|
||||
let trackUUID = false;
|
||||
if (route && route.match && route.match.params && route.match.params.uuid) {
|
||||
trackUUID = route.match.params.uuid;
|
||||
} else {
|
||||
trackUUID = false;
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!trackUUID) {
|
||||
return false;
|
||||
}
|
||||
|
||||
getTrack(trackUUID)
|
||||
.then(data => {
|
||||
setTrack(data);
|
||||
setLoading(false);
|
||||
})
|
||||
}, [trackUUID])
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="pageWrapper">
|
||||
<ScaleLoader color="#6AD7E5" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (!trackUUID || !track) {
|
||||
return (
|
||||
<div className="pageWrapper">
|
||||
Unable to fetch user
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
console.log(track)
|
||||
let length = "0";
|
||||
if (track.length && track.length !== '') {
|
||||
length = new Date(track.length * 1000).toISOString().substr(11, 8)
|
||||
}
|
||||
|
||||
let artists = [];
|
||||
for (let artist of track.artists) {
|
||||
const row = (
|
||||
<Link
|
||||
key={artist.uuid}
|
||||
to={"/artist/" + artist.uuid}
|
||||
>{artist.name} </Link>
|
||||
);
|
||||
artists.push(row);
|
||||
}
|
||||
|
||||
let albums = [];
|
||||
for (let album of track.albums) {
|
||||
const row = (
|
||||
<Link
|
||||
key={album.uuid}
|
||||
to={"/album/" + album.uuid}
|
||||
>{album.name} </Link>
|
||||
);
|
||||
albums.push(row);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="pageWrapper">
|
||||
<h1 style={{margin: 0}}>
|
||||
{track.name} {user && <Link
|
||||
key="editbuttonomg"
|
||||
to={"/track/" + trackUUID + "/edit"}
|
||||
>edit</Link>}
|
||||
</h1>
|
||||
<div className="pageBody">
|
||||
<div style={{display: `flex`, flexWrap: `wrap`, textAlign: `center`}}>
|
||||
<div style={{width: `300px`, padding: `0 10px 10px 10px`, textAlign: `left`}}>
|
||||
<img src={process.env.REACT_APP_API_URL + "/img/" + track.img + "_full.jpg"} alt={track.name} style={{maxWidth: `300px`, maxHeight: `300px`}}/>
|
||||
</div>
|
||||
<div style={{width: `290px`, padding: `0 10px 10px 10px`, margin: `0 5px 0 5px`, textAlign: `left`}}>
|
||||
<span style={{fontSize: '14pt'}}>
|
||||
{artists}
|
||||
</span>
|
||||
<br/>
|
||||
<span style={{fontSize: '14pt', textDecoration: 'none'}}>
|
||||
{albums}
|
||||
</span>
|
||||
<br/><br/>
|
||||
{track.mbid && <a rel="noreferrer" target="_blank" href={"https://musicbrainz.org/track/" + track.mbid}>Open on MusicBrainz<br/></a>}
|
||||
{track.spotify_id && <a rel="noreferrer" target="_blank" href={"https://open.spotify.com/track/" + track.spotify_id}>Open on Spotify<br/></a>}
|
||||
{length && <span>Track Length: {length}</span>}
|
||||
</div>
|
||||
<div style={{width: `290px`, padding: `0 10px 10px 10px`}}>
|
||||
<h3>Top 10 Scrobblers</h3>
|
||||
<TopUserTable uuid={track.uuid}/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Track;
|
@ -1,113 +0,0 @@
|
||||
import React, { useContext, useState, useEffect } from 'react';
|
||||
import '../App.css';
|
||||
import './TrackEdit.css';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import ScaleLoader from 'react-spinners/ScaleLoader';
|
||||
import { getTrack } from '../Api/index'
|
||||
import { Link } from 'react-router-dom';
|
||||
import AuthContext from '../Contexts/AuthContext';
|
||||
|
||||
const TrackEdit = (route) => {
|
||||
const history = useHistory();
|
||||
const { user } = useContext(AuthContext);
|
||||
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [track, setTrack] = useState({});
|
||||
|
||||
let trackUUID = false;
|
||||
if (route && route.match && route.match.params && route.match.params.uuid) {
|
||||
trackUUID = route.match.params.uuid;
|
||||
} else {
|
||||
trackUUID = false;
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!trackUUID) {
|
||||
return false;
|
||||
}
|
||||
|
||||
getTrack(trackUUID)
|
||||
.then(data => {
|
||||
setTrack(data);
|
||||
setLoading(false);
|
||||
})
|
||||
}, [trackUUID])
|
||||
|
||||
if (!user) {
|
||||
history.push("/login")
|
||||
}
|
||||
|
||||
if (user && !user.mod) {
|
||||
history.push("/Dashboard")
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="pageWrapper">
|
||||
<ScaleLoader color="#6AD7E5" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (!trackUUID || !track) {
|
||||
return (
|
||||
<div className="pageWrapper">
|
||||
Unable to fetch user
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
console.log(track)
|
||||
let length = "0";
|
||||
if (track.length && track.length !== '') {
|
||||
length = new Date(track.length * 1000).toISOString().substr(11, 8)
|
||||
}
|
||||
|
||||
let artists = [];
|
||||
for (let artist of track.artists) {
|
||||
const row = (
|
||||
<Link
|
||||
key={artist.uuid}
|
||||
to={"/artist/" + artist.uuid}
|
||||
>{artist.name} </Link>
|
||||
);
|
||||
artists.push(row);
|
||||
}
|
||||
|
||||
let albums = [];
|
||||
for (let album of track.albums) {
|
||||
const row = (
|
||||
<Link
|
||||
key={album.uuid}
|
||||
to={"/album/" + album.uuid}
|
||||
>{album.name} </Link>
|
||||
);
|
||||
albums.push(row);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="pageWrapper">
|
||||
<h1 style={{margin: 0}}>
|
||||
{track.name} {<Link
|
||||
key="editbuttonomg"
|
||||
to={"/track/" + trackUUID}
|
||||
>unedit</Link>}
|
||||
</h1>
|
||||
<div className="pageBody" style={{width: `900px`, textAlign: `center`}}>
|
||||
<img src={process.env.REACT_APP_API_URL + "/img/" + track.img + "_full.jpg"} alt={track.name} style={{maxWidth: `300px`, maxHeight: `300px`}}/>
|
||||
<br/>
|
||||
<label>Primary Artist ({track.artists[0].name}):</label><br/>
|
||||
<input type="text" value={track.artists[0].uuid} style={{width: `420px`}} disabled="true"/><br/>
|
||||
<label>Primary Album ({track.albums[0].name})</label><br/>
|
||||
<input type="text" value={track.albums[0].uuid} style={{width: `420px`}} disabled="true"/><br/>
|
||||
<br/>
|
||||
<label>MBID</label><br/>
|
||||
<input type="text" value={track.mbid} style={{width: `420px`}} /><br/>
|
||||
<label>Spotify ID</label><br/>
|
||||
<input type="text" value={track.spotify_id} style={{width: `420px`}} /><br/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default TrackEdit;
|
@ -1,45 +0,0 @@
|
||||
.userDropdown {
|
||||
color: #282C34;
|
||||
font-size: 12pt;
|
||||
}
|
||||
|
||||
.userButton {
|
||||
height: 50px;
|
||||
width: 100%;
|
||||
margin-top:-5px;
|
||||
}
|
||||
|
||||
.modal {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.modal > .header {
|
||||
width: 100%;
|
||||
border-bottom: 1px solid gray;
|
||||
font-size: 18px;
|
||||
text-align: center;
|
||||
padding: 5px;
|
||||
}
|
||||
.modal > .content {
|
||||
width: 100%;
|
||||
padding: 10px 5px;
|
||||
}
|
||||
.modal > .actions {
|
||||
width: 100%;
|
||||
padding: 10px 5px;
|
||||
margin: auto;
|
||||
text-align: center;
|
||||
}
|
||||
.modal > .close {
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
display: block;
|
||||
padding: 2px 5px;
|
||||
line-height: 20px;
|
||||
right: -10px;
|
||||
top: -10px;
|
||||
font-size: 24px;
|
||||
background: #ffffff;
|
||||
border-radius: 18px;
|
||||
border: 1px solid #cfcece;
|
||||
}
|
@ -1,278 +0,0 @@
|
||||
import React, { useContext, useState, useEffect } from 'react';
|
||||
import '../App.css';
|
||||
import './User.css';
|
||||
import { useHistory } from "react-router";
|
||||
import AuthContext from '../Contexts/AuthContext';
|
||||
import ScaleLoader from 'react-spinners/ScaleLoader';
|
||||
import { Button } from 'reactstrap';
|
||||
import { Formik, Form, Field } from 'formik';
|
||||
import { confirmAlert } from 'react-confirm-alert';
|
||||
import 'react-confirm-alert/src/react-confirm-alert.css';
|
||||
import {
|
||||
getUser,
|
||||
patchUser,
|
||||
spotifyConnectionRequest,
|
||||
spotifyDisonnectionRequest,
|
||||
navidromeDisonnectionRequest,
|
||||
navidromeConnectionRequest,
|
||||
} from '../Api/index'
|
||||
import TimezoneSelect from 'react-timezone-select'
|
||||
|
||||
const User = () => {
|
||||
const history = useHistory();
|
||||
const { user } = useContext(AuthContext);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [userdata, setUserdata] = useState({});
|
||||
|
||||
const updateTimezone = (vals) => {
|
||||
setUserdata({...userdata, timezone: vals});
|
||||
patchUser({timezone: vals.value})
|
||||
}
|
||||
|
||||
const resetTokenPopup = () => {
|
||||
confirmAlert({
|
||||
title: 'Reset token',
|
||||
message: 'Resetting your token will require you to update your Jellyfin server / custom scroblers with the new token. Continue?',
|
||||
buttons: [
|
||||
{
|
||||
label: 'Reset',
|
||||
onClick: () => resetToken()
|
||||
},
|
||||
{
|
||||
label: 'No',
|
||||
}
|
||||
]
|
||||
});
|
||||
};
|
||||
|
||||
const connectNavidromePopup = () => {
|
||||
confirmAlert({
|
||||
title: 'Connect Navidrome',
|
||||
buttons: [
|
||||
{
|
||||
label: 'Close',
|
||||
}
|
||||
],
|
||||
childrenElement: () => <Formik
|
||||
initialValues={{ url: '', username: '', password: '' }}
|
||||
onSubmit={values => navidromeConnectionRequest(values)}
|
||||
>
|
||||
<Form>
|
||||
<label>
|
||||
Server URL<br/>
|
||||
<Field
|
||||
name="url"
|
||||
type="text"
|
||||
/>
|
||||
</label>
|
||||
<br/>
|
||||
<label>
|
||||
Username<br/>
|
||||
<Field
|
||||
name="username"
|
||||
type="text"
|
||||
/>
|
||||
</label>
|
||||
<br/>
|
||||
<label>
|
||||
Password<br/>
|
||||
<Field
|
||||
name="password"
|
||||
type="password"
|
||||
/>
|
||||
</label>
|
||||
<br/><br/>
|
||||
<Button
|
||||
color="primary"
|
||||
type="submit"
|
||||
className="loginButton"
|
||||
>Connect</Button>
|
||||
</Form>
|
||||
</Formik>,
|
||||
});
|
||||
};
|
||||
|
||||
const disconnectNavidromePopup = () => {
|
||||
confirmAlert({
|
||||
title: 'Disconnect Navidrome',
|
||||
message: 'Are you sure you want to disconnect your Navidrome connection?',
|
||||
buttons: [
|
||||
{
|
||||
label: 'Disconnect',
|
||||
onClick: () => navidromeDisonnectionRequest()
|
||||
},
|
||||
{
|
||||
label: 'No',
|
||||
}
|
||||
]
|
||||
});
|
||||
};
|
||||
|
||||
const disconnectSpotifyPopup = () => {
|
||||
confirmAlert({
|
||||
title: 'Disconnect Spotify',
|
||||
message: 'Are you sure you want to disconnect your Spotify account?',
|
||||
buttons: [
|
||||
{
|
||||
label: 'Disconnect',
|
||||
onClick: () => spotifyDisonnectionRequest()
|
||||
},
|
||||
{
|
||||
label: 'No',
|
||||
}
|
||||
]
|
||||
});
|
||||
};
|
||||
|
||||
const connectJellyfinPopup = () => {
|
||||
confirmAlert({
|
||||
title: 'Connect Jellyfin',
|
||||
message: 'Install the webhook plugin. Add a webhook to {API_URL}/api/v1/ingress/jellyfin?key='+userdata.token
|
||||
+'\nSet it to only send "Playback Start" and "Songs/Albums"',
|
||||
buttons: [
|
||||
{
|
||||
label: 'Close',
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
const connectOtherPopup = () => {
|
||||
confirmAlert({
|
||||
title: 'Connect Jellyfin',
|
||||
message: 'Endpoint: {API_URL}/api/v1/ingress/multiscrobbler?key='+userdata.token
|
||||
+'\nNeed to send JSON body with a string array for artists names, album:string, track:string, playDate:timestamp of scrobble, duration:tracklength in seconds',
|
||||
buttons: [
|
||||
{
|
||||
label: 'Close',
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
const resetToken = () => {
|
||||
setLoading(true);
|
||||
patchUser({ token: '' })
|
||||
.then(() => {
|
||||
getUser()
|
||||
.then(data => {
|
||||
setUserdata(data);
|
||||
setLoading(false);
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!user) {
|
||||
return
|
||||
}
|
||||
|
||||
getUser()
|
||||
.then(data => {
|
||||
setUserdata(data);
|
||||
setLoading(false);
|
||||
})
|
||||
}, [user])
|
||||
|
||||
if (!user) {
|
||||
history.push("/login")
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="pageWrapper">
|
||||
<ScaleLoader color="#6AD7E5" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="pageWrapper">
|
||||
<h1>
|
||||
Welcome {userdata.username}
|
||||
</h1>
|
||||
<div style={{display: `flex`, flexWrap: `wrap`, textAlign: `center`}}>
|
||||
<div style={{width: `300px`, padding: `0 10px 10px 10px`, textAlign: `left`}}>
|
||||
<h3 style={{textAlign: `center`}}>Profile</h3>
|
||||
Timezone<br/>
|
||||
<TimezoneSelect
|
||||
className="userDropdown"
|
||||
value={userdata.timezone}
|
||||
onChange={updateTimezone}
|
||||
/><br/>
|
||||
Created At:<br/>{userdata.created_at}<br/>
|
||||
Email:<br/>{userdata.email}<br/>
|
||||
Verified: {userdata.verified ? '✓' : '✖'}
|
||||
</div>
|
||||
<div style={{width: `300px`, padding: `0 10px 10px 10px`}}>
|
||||
<h3>Scrobblers</h3>
|
||||
<br/>
|
||||
{userdata.spotify_username
|
||||
? <Button
|
||||
color="secondary"
|
||||
type="button"
|
||||
className="userButton"
|
||||
onClick={disconnectSpotifyPopup}
|
||||
>Disconnect Spotify ({userdata.spotify_username})</Button>
|
||||
: <div>
|
||||
<br/>
|
||||
<Button
|
||||
color="primary"
|
||||
type="button"
|
||||
className="userButton"
|
||||
onClick={spotifyConnectionRequest}
|
||||
>Connect To Spotify</Button>
|
||||
</div>
|
||||
}
|
||||
<br/><br/>
|
||||
{userdata.navidrome_server
|
||||
? <Button
|
||||
color="secondary"
|
||||
type="button"
|
||||
className="userButton"
|
||||
onClick={disconnectNavidromePopup}
|
||||
>Disconnect Navidrome ({userdata.navidrome_server})</Button>
|
||||
: <Button
|
||||
color="primary"
|
||||
type="button"
|
||||
className="userButton"
|
||||
onClick={connectNavidromePopup}
|
||||
>Connect Navidrome</Button>
|
||||
}
|
||||
<br/><br/>
|
||||
<Button
|
||||
color="primary"
|
||||
type="button"
|
||||
className="userButton"
|
||||
onClick={connectJellyfinPopup}
|
||||
>Connect Jellyfin</Button>
|
||||
<br/><br/>
|
||||
<Button
|
||||
color="primary"
|
||||
type="button"
|
||||
className="userButton"
|
||||
onClick={connectOtherPopup}
|
||||
>Other Scrobblers</Button>
|
||||
</div>
|
||||
<div style={{width: `300px`, padding: `0 10px 10px 10px`}}>
|
||||
<h3>Sad Settings</h3>
|
||||
<br/>
|
||||
<Button
|
||||
color="secondary"
|
||||
type="button"
|
||||
className="userButton"
|
||||
>Delete Account</Button>
|
||||
<br/><br/>
|
||||
<Button
|
||||
color="secondary"
|
||||
type="button"
|
||||
className="userButton"
|
||||
onClick={resetTokenPopup}
|
||||
>Reset Scrobbler Token</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default User;
|
@ -1,13 +0,0 @@
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||
monospace;
|
||||
}
|
@ -1,30 +0,0 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import './index.css';
|
||||
import App from './App';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
|
||||
import { ToastContainer } from 'react-toastify';
|
||||
import 'react-toastify/dist/ReactToastify.min.css';
|
||||
|
||||
import AuthContextProvider from './Contexts/AuthContextProvider';
|
||||
|
||||
ReactDOM.render(
|
||||
<AuthContextProvider>
|
||||
<BrowserRouter>
|
||||
<ToastContainer
|
||||
position="bottom-right"
|
||||
autoClose={5000}
|
||||
hideProgressBar={false}
|
||||
newestOnTop={true}
|
||||
closeOnClick
|
||||
rtl={false}
|
||||
pauseOnFocusLoss={false}
|
||||
draggable
|
||||
pauseOnHover
|
||||
/>
|
||||
<App />
|
||||
</BrowserRouter>
|
||||
</AuthContextProvider>,
|
||||
document.getElementById('root')
|
||||
);
|
BIN
web/src/logo.png
BIN
web/src/logo.png
Binary file not shown.
Before Width: | Height: | Size: 51 KiB |
@ -1,13 +0,0 @@
|
||||
const reportWebVitals = onPerfEntry => {
|
||||
if (onPerfEntry && onPerfEntry instanceof Function) {
|
||||
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
|
||||
getCLS(onPerfEntry);
|
||||
getFID(onPerfEntry);
|
||||
getFCP(onPerfEntry);
|
||||
getLCP(onPerfEntry);
|
||||
getTTFB(onPerfEntry);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export default reportWebVitals;
|
@ -1,5 +0,0 @@
|
||||
// jest-dom adds custom jest matchers for asserting on DOM nodes.
|
||||
// allows you to do things like:
|
||||
// expect(element).toHaveTextContent(/react/i)
|
||||
// learn more: https://github.com/testing-library/jest-dom
|
||||
import '@testing-library/jest-dom';
|
Loading…
Reference in New Issue
Block a user