- 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:
Daniel Mason 2021-04-01 23:17:46 +13:00
parent af02bd99cc
commit e570314ac2
Signed by: idanoo
GPG key ID: 387387CDBC02F132
27 changed files with 435 additions and 89 deletions

View file

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

View file

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

View file

@ -1,3 +1,7 @@
html, body {
background-color: #282c34;
}
.App {
text-align: center;
}

View file

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

View file

@ -11,8 +11,10 @@ const HomeBanner = () => {
useEffect(() => {
getStats()
.then(data => {
setBannerData(data);
setIsLoading(false);
if (data.users) {
setBannerData(data);
setIsLoading(false);
}
})
}, [])

View file

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

View file

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

View file

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

View file

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

View file

@ -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
View file

@ -0,0 +1,4 @@
.userBody {
padding: 20px 5px 5px 5px;
font-size: 16pt;
}

53
web/src/Pages/User.js Normal file
View 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;

View file

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