0.1.001 Split out web to separate repo

This commit is contained in:
Daniel Mason 2021-12-25 12:05:57 +13:00
parent 525d5c92b5
commit d268d939eb
65 changed files with 11 additions and 42509 deletions

View file

@ -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)
});
}

View file

@ -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);
}
}

View file

@ -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);

View file

@ -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();
});

View file

@ -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;
}

View file

@ -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;

View file

@ -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;
}

View file

@ -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;

View file

@ -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;

View file

@ -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;
}

View file

@ -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;

View file

@ -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;
}

View file

@ -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;

View file

@ -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;

View file

@ -1,5 +0,0 @@
import React from 'react';
const AuthContext = React.createContext();
export default AuthContext;

View file

@ -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;

View file

@ -1,3 +0,0 @@
import { createBrowserHistory } from "history";
export const history = createBrowserHistory();

View file

@ -1 +0,0 @@

View file

@ -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;

View file

@ -1,9 +0,0 @@
.adminFields {
width: 100%;
}
.admin {
height: 50px;
width: 100%;
margin-top:-5px;
}

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -1,11 +0,0 @@
.homeText {
margin: 0;
font-size: 2rem;
}
.subHomeText {
margin-top: -5px;
font-style: italic;
color: #CCC;
font-size: 1.4rem;
}

View file

@ -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;

View file

@ -1,9 +0,0 @@
.loginFields {
width: 100%;
}
.loginButton {
height: 50px;
width: 100%;
margin-top:-5px;
}

View file

@ -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;

View file

@ -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;

View file

@ -1,9 +0,0 @@
.registerFields {
width: 100%;
}
.registerButton {
height: 50px;
width: 100%;
margin-top:-5px;
}

View file

@ -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;

View file

@ -1,9 +0,0 @@
.resetFields {
width: 100%;
}
.resetButton {
height: 50px;
width: 100%;
margin-top:-5px;
}

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;
}

View file

@ -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;

View file

@ -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;
}

View file

@ -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')
);

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

View file

@ -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;

View file

@ -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';