mirror of
https://github.com/idanoo/GoScrobble
synced 2025-07-01 21:52:19 +00:00
0.1.001 Split out web to separate repo
This commit is contained in:
parent
525d5c92b5
commit
d268d939eb
65 changed files with 11 additions and 42509 deletions
|
@ -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…
Add table
Add a link
Reference in a new issue