mirror of
https://github.com/idanoo/GoScrobble
synced 2025-07-01 05:32:18 +00:00
0.0.9
- Fix mobile menu auto collapse on select - Add /u/ route for public user profiles (Added private flag to db - to implement later) - Add /user route for your own profile / edit profile - Added handling for if API is offline/incorrect - Add index.html loading spinner while react bundle downloads - Change HashRouter to BrowserRouter - Added sources column to scrobbles
This commit is contained in:
parent
af02bd99cc
commit
e570314ac2
27 changed files with 435 additions and 89 deletions
|
@ -24,20 +24,42 @@
|
|||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<style>
|
||||
html, body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
background-color: #282C34;
|
||||
}
|
||||
|
||||
.loader-container {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 25%;
|
||||
transform: translate(-50%, -25%);
|
||||
}
|
||||
|
||||
.loader {
|
||||
border: 16px solid #282C34;
|
||||
border-top: 16px solid #3498db;
|
||||
border-radius: 50%;
|
||||
width: 130px;
|
||||
height: 130px;
|
||||
animation: spin 2s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
</style>
|
||||
<title>GoScrobble</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
||||
You can add webfonts, meta tags, or analytics to this file.
|
||||
The build step will place the bundled scripts into the <body> tag.
|
||||
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
<div class="loader-container">
|
||||
<div class="loader"></div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
52
web/public/loader.svg
Normal file
52
web/public/loader.svg
Normal file
|
@ -0,0 +1,52 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="margin: auto; background: rgb(40, 44, 52) none repeat scroll 0% 0%; display: block; shape-rendering: auto;" width="200px" height="200px" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid">
|
||||
<g transform="rotate(0 50 50)">
|
||||
<rect x="47" y="6.5" rx="3" ry="4.41" width="6" height="21" fill="#6ad7e5">
|
||||
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.9166666666666666s" repeatCount="indefinite"></animate>
|
||||
</rect>
|
||||
</g><g transform="rotate(30 50 50)">
|
||||
<rect x="47" y="6.5" rx="3" ry="4.41" width="6" height="21" fill="#6ad7e5">
|
||||
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.8333333333333334s" repeatCount="indefinite"></animate>
|
||||
</rect>
|
||||
</g><g transform="rotate(60 50 50)">
|
||||
<rect x="47" y="6.5" rx="3" ry="4.41" width="6" height="21" fill="#6ad7e5">
|
||||
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.75s" repeatCount="indefinite"></animate>
|
||||
</rect>
|
||||
</g><g transform="rotate(90 50 50)">
|
||||
<rect x="47" y="6.5" rx="3" ry="4.41" width="6" height="21" fill="#6ad7e5">
|
||||
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.6666666666666666s" repeatCount="indefinite"></animate>
|
||||
</rect>
|
||||
</g><g transform="rotate(120 50 50)">
|
||||
<rect x="47" y="6.5" rx="3" ry="4.41" width="6" height="21" fill="#6ad7e5">
|
||||
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.5833333333333334s" repeatCount="indefinite"></animate>
|
||||
</rect>
|
||||
</g><g transform="rotate(150 50 50)">
|
||||
<rect x="47" y="6.5" rx="3" ry="4.41" width="6" height="21" fill="#6ad7e5">
|
||||
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.5s" repeatCount="indefinite"></animate>
|
||||
</rect>
|
||||
</g><g transform="rotate(180 50 50)">
|
||||
<rect x="47" y="6.5" rx="3" ry="4.41" width="6" height="21" fill="#6ad7e5">
|
||||
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.4166666666666667s" repeatCount="indefinite"></animate>
|
||||
</rect>
|
||||
</g><g transform="rotate(210 50 50)">
|
||||
<rect x="47" y="6.5" rx="3" ry="4.41" width="6" height="21" fill="#6ad7e5">
|
||||
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.3333333333333333s" repeatCount="indefinite"></animate>
|
||||
</rect>
|
||||
</g><g transform="rotate(240 50 50)">
|
||||
<rect x="47" y="6.5" rx="3" ry="4.41" width="6" height="21" fill="#6ad7e5">
|
||||
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.25s" repeatCount="indefinite"></animate>
|
||||
</rect>
|
||||
</g><g transform="rotate(270 50 50)">
|
||||
<rect x="47" y="6.5" rx="3" ry="4.41" width="6" height="21" fill="#6ad7e5">
|
||||
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.16666666666666666s" repeatCount="indefinite"></animate>
|
||||
</rect>
|
||||
</g><g transform="rotate(300 50 50)">
|
||||
<rect x="47" y="6.5" rx="3" ry="4.41" width="6" height="21" fill="#6ad7e5">
|
||||
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.08333333333333333s" repeatCount="indefinite"></animate>
|
||||
</rect>
|
||||
</g><g transform="rotate(330 50 50)">
|
||||
<rect x="47" y="6.5" rx="3" ry="4.41" width="6" height="21" fill="#6ad7e5">
|
||||
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="0s" repeatCount="indefinite"></animate>
|
||||
</rect>
|
||||
</g>
|
||||
<!-- [ldio] generated by https://loading.io/ --></svg>
|
After Width: | Height: | Size: 3.4 KiB |
|
@ -14,8 +14,6 @@ function getHeaders() {
|
|||
}
|
||||
|
||||
export const PostLogin = (formValues) => {
|
||||
// const { setLoading, setUser } = useContext(AuthContext);
|
||||
// setLoading(true)
|
||||
return axios.post(process.env.REACT_APP_API_URL + "login", formValues)
|
||||
.then((response) => {
|
||||
if (response.data.token) {
|
||||
|
@ -36,6 +34,7 @@ export const PostLogin = (formValues) => {
|
|||
}
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error('Failed to connect');
|
||||
return Promise.resolve();
|
||||
});
|
||||
};
|
||||
|
@ -54,6 +53,7 @@ export const PostRegister = (formValues) => {
|
|||
}
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error('Failed to connect');
|
||||
return Promise.resolve();
|
||||
});
|
||||
};
|
||||
|
@ -61,16 +61,21 @@ export const PostRegister = (formValues) => {
|
|||
export const getStats = () => {
|
||||
return axios.get(process.env.REACT_APP_API_URL + "stats").then(
|
||||
(data) => {
|
||||
data.isLoading = false;
|
||||
return data.data;
|
||||
}
|
||||
);
|
||||
).catch(() => {
|
||||
toast.error('Failed to connect');
|
||||
return {};
|
||||
});
|
||||
};
|
||||
|
||||
export const getRecentScrobbles = (id) => {
|
||||
return axios.get(process.env.REACT_APP_API_URL + "user/" + id + "/scrobbles", { headers: getHeaders() })
|
||||
.then((data) => {
|
||||
return data.data;
|
||||
}).catch(() => {
|
||||
toast.error('Failed to connect');
|
||||
return {};
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -78,6 +83,9 @@ export const getConfigs = () => {
|
|||
return axios.get(process.env.REACT_APP_API_URL + "config", { headers: getHeaders() })
|
||||
.then((data) => {
|
||||
return data.data;
|
||||
}).catch(() => {
|
||||
toast.error('Failed to connect');
|
||||
return {};
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -101,3 +109,22 @@ export const postConfigs = (values, toggle) => {
|
|||
});
|
||||
};
|
||||
|
||||
export const getProfile = (userName) => {
|
||||
return axios.get(process.env.REACT_APP_API_URL + "profile/" + userName, { headers: getHeaders() })
|
||||
.then((data) => {
|
||||
return data.data;
|
||||
}).catch(() => {
|
||||
toast.error('Failed to connect');
|
||||
return {};
|
||||
});
|
||||
};
|
||||
|
||||
export const getUser = () => {
|
||||
return axios.get(process.env.REACT_APP_API_URL + "user", { headers: getHeaders() })
|
||||
.then((data) => {
|
||||
return data.data;
|
||||
}).catch(() => {
|
||||
toast.error('Failed to connect');
|
||||
return {};
|
||||
});
|
||||
};
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
html, body {
|
||||
background-color: #282c34;
|
||||
}
|
||||
|
||||
.App {
|
||||
text-align: center;
|
||||
}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { Route, Switch, withRouter } from 'react-router-dom';
|
||||
|
||||
import Home from './Pages/Home';
|
||||
import About from './Pages/About';
|
||||
import Dashboard from './Pages/Dashboard';
|
||||
import Profile from './Pages/Profile';
|
||||
import User from './Pages/User';
|
||||
import Admin from './Pages/Admin';
|
||||
import Login from './Pages/Login';
|
||||
import Register from './Pages/Register';
|
||||
|
@ -14,7 +14,13 @@ import 'bootstrap/dist/css/bootstrap.min.css';
|
|||
import './App.css';
|
||||
|
||||
const App = () => {
|
||||
let boolTrue = true
|
||||
let boolTrue = true;
|
||||
|
||||
// Remove loading spinner on load
|
||||
const el = document.querySelector(".loader-container");
|
||||
if (el) {
|
||||
el.remove();
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
@ -24,7 +30,8 @@ const App = () => {
|
|||
<Route path="/about" component={About} />
|
||||
|
||||
<Route path="/dashboard" component={Dashboard} />
|
||||
<Route path="/profile" component={Profile} />
|
||||
<Route path="/user" component={User} />
|
||||
<Route path="/u/:uuid" component={Profile} />
|
||||
|
||||
<Route path="/admin" component={Admin} />
|
||||
|
||||
|
|
|
@ -11,8 +11,10 @@ const HomeBanner = () => {
|
|||
useEffect(() => {
|
||||
getStats()
|
||||
.then(data => {
|
||||
setBannerData(data);
|
||||
setIsLoading(false);
|
||||
if (data.users) {
|
||||
setBannerData(data);
|
||||
setIsLoading(false);
|
||||
}
|
||||
})
|
||||
}, [])
|
||||
|
||||
|
|
|
@ -45,38 +45,41 @@ const Navigation = () => {
|
|||
{user ?
|
||||
<Nav className="navLinkLoginMobile" navbar>
|
||||
{loggedInMenuItems.map(menuItem =>
|
||||
<NavItem>
|
||||
<Link
|
||||
<NavItem key={menuItem}>
|
||||
<Link
|
||||
key={menuItem}
|
||||
className="navLinkMobile"
|
||||
style={active === menuItem ? activeStyle : {}}
|
||||
to={menuItem}
|
||||
onClick={toggleCollapsed}
|
||||
>{menuItem}</Link>
|
||||
</NavItem>
|
||||
)}
|
||||
<Link
|
||||
to="/profile"
|
||||
style={active === "profile" ? activeStyle : {}}
|
||||
to="/user"
|
||||
style={active === "user" ? activeStyle : {}}
|
||||
className="navLinkMobile"
|
||||
>Profile</Link>
|
||||
onClick={toggleCollapsed}
|
||||
>{user.username}</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>
|
||||
<NavItem key={menuItem}>
|
||||
<Link
|
||||
key={menuItem}
|
||||
className="navLinkMobile"
|
||||
style={active === menuItem ? activeStyle : {}}
|
||||
to={menuItem === "Home" ? "/" : menuItem}
|
||||
>
|
||||
{menuItem}
|
||||
onClick={toggleCollapsed}
|
||||
>{menuItem}
|
||||
</Link>
|
||||
</NavItem>
|
||||
)}
|
||||
|
@ -85,6 +88,7 @@ const Navigation = () => {
|
|||
to="/Login"
|
||||
style={active === "Login" ? activeStyle : {}}
|
||||
className="navLinkMobile"
|
||||
onClick={toggleCollapsed}
|
||||
>Login</Link>
|
||||
</NavItem>
|
||||
<NavItem>
|
||||
|
@ -92,6 +96,7 @@ const Navigation = () => {
|
|||
to="/Register"
|
||||
className="navLinkMobile"
|
||||
style={active === "Register" ? activeStyle : {}}
|
||||
onClick={toggleCollapsed}
|
||||
>Register</Link>
|
||||
</NavItem>
|
||||
</Nav>
|
||||
|
@ -132,8 +137,8 @@ const Navigation = () => {
|
|||
{user ?
|
||||
<div className="navLinkLogin">
|
||||
<Link
|
||||
to="/profile"
|
||||
style={active === "profile" ? activeStyle : {}}
|
||||
to="/user"
|
||||
style={active === "user" ? activeStyle : {}}
|
||||
className="navLink"
|
||||
>{user.username}</Link>
|
||||
{user.admin &&
|
||||
|
|
|
@ -14,9 +14,9 @@ const ScrobbleTable = (props) => {
|
|||
</thead>
|
||||
<tbody>
|
||||
{
|
||||
props.data && props.data.items &&
|
||||
props.data.items.map(function (element) {
|
||||
return <tr>
|
||||
props.data &&
|
||||
props.data.map(function (element) {
|
||||
return <tr key={element.uuid}>
|
||||
<td>{element.time}</td>
|
||||
<td>{element.track}</td>
|
||||
<td>{element.artist}</td>
|
||||
|
|
|
@ -17,19 +17,17 @@ const Admin = () => {
|
|||
useEffect(() => {
|
||||
getConfigs()
|
||||
.then(data => {
|
||||
setConfigs(data.configs);
|
||||
setToggle(data.configs.REGISTRATION_ENABLED === "1")
|
||||
if (data.configs) {
|
||||
setConfigs(data.configs);
|
||||
setToggle(data.configs.REGISTRATION_ENABLED === "1")
|
||||
}
|
||||
setLoading(false);
|
||||
})
|
||||
}, [])
|
||||
|
||||
if (!user || !user.admin) {
|
||||
return (
|
||||
<div className="pageWrapper">
|
||||
<h1>Unauthorized</h1>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
const handleToggle = () => {
|
||||
setToggle(!toggle);
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
|
@ -39,9 +37,13 @@ const Admin = () => {
|
|||
)
|
||||
}
|
||||
|
||||
const handleToggle = () => {
|
||||
setToggle(!toggle);
|
||||
};
|
||||
if (!user || !user.admin) {
|
||||
return (
|
||||
<div className="pageWrapper">
|
||||
<h1>Unauthorized</h1>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="pageWrapper">
|
||||
|
|
|
@ -13,10 +13,6 @@ const Dashboard = () => {
|
|||
let [loading, setLoading] = useState(true);
|
||||
let [dashboardData, setDashboardData] = useState({});
|
||||
|
||||
if (!user) {
|
||||
history.push("/login");
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!user) {
|
||||
return
|
||||
|
@ -28,14 +24,22 @@ const Dashboard = () => {
|
|||
})
|
||||
}, [user])
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="pageWrapper">
|
||||
<ScaleLoader color="#6AD7E5" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="pageWrapper">
|
||||
<h1>
|
||||
Dashboard!
|
||||
{user.username}'s Dashboard!
|
||||
</h1>
|
||||
{loading
|
||||
? <ScaleLoader color="#6AD7E5" size={60} />
|
||||
: <ScrobbleTable data={dashboardData} />
|
||||
: <ScrobbleTable data={dashboardData.items} />
|
||||
}
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -1,25 +1,59 @@
|
|||
import React, { useContext } from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import '../App.css';
|
||||
import './Dashboard.css';
|
||||
import { useHistory } from "react-router";
|
||||
import AuthContext from '../Contexts/AuthContext';
|
||||
import './Profile.css';
|
||||
import ScaleLoader from 'react-spinners/ScaleLoader';
|
||||
import { getProfile } from '../Api/index'
|
||||
import ScrobbleTable from '../Components/ScrobbleTable'
|
||||
|
||||
const Profile = () => {
|
||||
const history = useHistory();
|
||||
const { user } = useContext(AuthContext);
|
||||
const Profile = (route) => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [profile, setProfile] = useState({});
|
||||
|
||||
if (!user) {
|
||||
history.push("/login");
|
||||
let username = false;
|
||||
if (route && route.match && route.match.params && route.match.params.uuid) {
|
||||
username = route.match.params.uuid
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!username) {
|
||||
return false;
|
||||
}
|
||||
|
||||
getProfile(username)
|
||||
.then(data => {
|
||||
setProfile(data);
|
||||
console.log(data)
|
||||
setLoading(false);
|
||||
})
|
||||
}, [username])
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="pageWrapper">
|
||||
<ScaleLoader color="#6AD7E5" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (!username || Object.keys(profile).length === 0) {
|
||||
return (
|
||||
<div className="pageWrapper">
|
||||
Unable to fetch user
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="pageWrapper">
|
||||
<h1>
|
||||
Welcome {user.username}!
|
||||
{profile.username}'s Profile
|
||||
</h1>
|
||||
<div className="profileBody">
|
||||
Last 10 scrobbles...<br/>
|
||||
<ScrobbleTable data={profile.scrobbles}/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
export default Profile;
|
4
web/src/Pages/User.css
Normal file
4
web/src/Pages/User.css
Normal file
|
@ -0,0 +1,4 @@
|
|||
.userBody {
|
||||
padding: 20px 5px 5px 5px;
|
||||
font-size: 16pt;
|
||||
}
|
53
web/src/Pages/User.js
Normal file
53
web/src/Pages/User.js
Normal file
|
@ -0,0 +1,53 @@
|
|||
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 { getUser } from '../Api/index'
|
||||
|
||||
const User = () => {
|
||||
const history = useHistory();
|
||||
const { user } = useContext(AuthContext);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [userdata, setUserdata] = useState({});
|
||||
|
||||
useEffect(() => {
|
||||
if (!user) {
|
||||
return
|
||||
}
|
||||
|
||||
getUser()
|
||||
.then(data => {
|
||||
setUserdata(data);
|
||||
setLoading(false);
|
||||
})
|
||||
}, [user])
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="pageWrapper">
|
||||
<ScaleLoader color="#6AD7E5" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
history.push("/login")
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="pageWrapper">
|
||||
<h1>
|
||||
Welcome {userdata.username}
|
||||
</h1>
|
||||
<p className="userBody">
|
||||
Created At: {userdata.created_at}<br/>
|
||||
Email: {userdata.email}<br/>
|
||||
Verified: {userdata.verified ? '✓' : '✖'}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default User;
|
|
@ -2,7 +2,7 @@ import React from 'react';
|
|||
import ReactDOM from 'react-dom';
|
||||
import './index.css';
|
||||
import App from './App';
|
||||
import { HashRouter } from 'react-router-dom';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
|
||||
import { ToastContainer } from 'react-toastify';
|
||||
import 'react-toastify/dist/ReactToastify.min.css';
|
||||
|
@ -11,7 +11,7 @@ import AuthContextProvider from './Contexts/AuthContextProvider';
|
|||
|
||||
ReactDOM.render(
|
||||
<AuthContextProvider>
|
||||
<HashRouter>
|
||||
<BrowserRouter>
|
||||
<ToastContainer
|
||||
position="bottom-right"
|
||||
autoClose={5000}
|
||||
|
@ -24,7 +24,7 @@ ReactDOM.render(
|
|||
pauseOnHover
|
||||
/>
|
||||
<App />
|
||||
</HashRouter>
|
||||
</BrowserRouter>
|
||||
</AuthContextProvider>,
|
||||
document.getElementById('root')
|
||||
);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue