- 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

@ -3,7 +3,7 @@ stages:
- bundle
variables:
VERSION: 0.0.8
VERSION: 0.0.9
build-go:
image: golang:1.16.2

View File

@ -1,3 +1,12 @@
# 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
# 0.0.8
- Added Admin/Site config page in frontend for admin users
- Added API POST/GET /config endpoint

View File

@ -0,0 +1,4 @@
package goscrobble
func getImageLastFM(src string) {
}

View File

@ -50,7 +50,7 @@ func ParseJellyfinInput(userUUID string, data map[string]interface{}, ip net.IP,
}
// Insert album if not exist
err = insertScrobble(userUUID, track.Uuid, ip, tx)
err = insertScrobble(userUUID, track.Uuid, "jellyfin", ip, tx)
if err != nil {
log.Printf("%+v", err)
return errors.New("Failed to map track")

View File

@ -0,0 +1,21 @@
package goscrobble
type ProfileResponse struct {
UUID string `json:"uuid"`
Username string `json:"username"`
Scrobbles []ScrobbleRequestItem `json:"scrobbles"`
}
func getProfile(user User) (ProfileResponse, error) {
resp := ProfileResponse{
UUID: user.UUID,
Username: user.Username,
}
scrobbleReq, err := fetchScrobblesForUser(user.UUID, 10, 1)
if err != nil {
return resp, err
}
resp.Scrobbles = scrobbleReq.Items
return resp, nil
}

View File

@ -36,8 +36,8 @@ type ScrobbleRequestItem struct {
}
// insertScrobble - This will return if it exists or create it based on MBID > Name
func insertScrobble(user string, track string, ip net.IP, tx *sql.Tx) error {
err := insertNewScrobble(user, track, ip, tx)
func insertScrobble(user string, track string, source string, ip net.IP, tx *sql.Tx) error {
err := insertNewScrobble(user, track, source, ip, tx)
if err != nil {
log.Printf("Error inserting scrobble %s %+v", user, err)
return errors.New("Failed to insert scrobble!")
@ -46,7 +46,7 @@ func insertScrobble(user string, track string, ip net.IP, tx *sql.Tx) error {
return nil
}
func fetchScrobblesForUser(userUuid string, page int) (ScrobbleRequest, error) {
func fetchScrobblesForUser(userUuid string, limit int, page int) (ScrobbleRequest, error) {
scrobbleReq := ScrobbleRequest{}
var count int
@ -76,8 +76,8 @@ func fetchScrobblesForUser(userUuid string, page int) (ScrobbleRequest, error) {
"JOIN albums ON track_album.album = albums.uuid "+
"JOIN users ON scrobbles.user = users.uuid "+
"WHERE user = UUID_TO_BIN(?, true) "+
"ORDER BY scrobbles.created_at DESC LIMIT 500",
userUuid)
"ORDER BY scrobbles.created_at DESC LIMIT ?",
userUuid, limit)
if err != nil {
log.Printf("Failed to fetch scrobbles: %+v", err)
return scrobbleReq, errors.New("Failed to fetch scrobbles")
@ -108,9 +108,9 @@ func fetchScrobblesForUser(userUuid string, page int) (ScrobbleRequest, error) {
return scrobbleReq, nil
}
func insertNewScrobble(user string, track string, ip net.IP, tx *sql.Tx) error {
_, err := tx.Exec("INSERT INTO `scrobbles` (`uuid`, `created_at`, `created_ip`, `user`, `track`) "+
"VALUES (UUID_TO_BIN(UUID(), true), NOW(), ?, UUID_TO_BIN(?, true),UUID_TO_BIN(?, true))", ip, user, track)
func insertNewScrobble(user string, track string, source string, ip net.IP, tx *sql.Tx) error {
_, err := tx.Exec("INSERT INTO `scrobbles` (`uuid`, `created_at`, `created_ip`, `user`, `track`, `source`) "+
"VALUES (UUID_TO_BIN(UUID(), true), NOW(), ?, UUID_TO_BIN(?, true),UUID_TO_BIN(?, true), ?)", ip, user, track, source)
return err
}

View File

@ -25,11 +25,14 @@ type jsonResponse struct {
Msg string `json:"message,omitempty"`
}
// Limits to 1 req / 10 sec
// Limits to 1 req / 4 sec
var heavyLimiter = NewIPRateLimiter(0.25, 2)
// Limits to 5 req / sec
var standardLimiter = NewIPRateLimiter(1, 5)
var standardLimiter = NewIPRateLimiter(5, 5)
// Limits to 10 req / sec
var lightLimiter = NewIPRateLimiter(10, 10)
// List of Reverse proxies
var ReverseProxies []string
@ -48,17 +51,21 @@ func HandleRequests(port string) {
// Static Token for /ingress
v1.HandleFunc("/ingress/jellyfin", tokenMiddleware(handleIngress)).Methods("POST")
// JWT Auth
v1.HandleFunc("/user/{id}/scrobbles", jwtMiddleware(fetchScrobbleResponse)).Methods("GET")
// JWT Auth - PWN PROFILE ONLY.
v1.HandleFunc("/user", jwtMiddleware(fetchUser)).Methods("GET")
// v1.HandleFunc("/user", jwtMiddleware(fetchScrobbleResponse)).Methods("PATCH")
v1.HandleFunc("/user/{uuid}/scrobbles", jwtMiddleware(fetchScrobbleResponse)).Methods("GET")
// Config auth
v1.HandleFunc("/config", adminMiddleware(fetchConfig)).Methods("GET")
v1.HandleFunc("/config", adminMiddleware(postConfig)).Methods("POST")
// No Auth
v1.HandleFunc("/stats", handleStats).Methods("GET")
v1.HandleFunc("/profile/{username}", limitMiddleware(fetchProfile, lightLimiter)).Methods("GET")
v1.HandleFunc("/register", limitMiddleware(handleRegister, heavyLimiter)).Methods("POST")
v1.HandleFunc("/login", limitMiddleware(handleLogin, standardLimiter)).Methods("POST")
v1.HandleFunc("/stats", handleStats).Methods("GET")
// This just prevents it serving frontend stuff over /api
r.PathPrefix("/api")
@ -80,7 +87,7 @@ func HandleRequests(port string) {
log.Fatal(http.ListenAndServe(":"+port, handler))
}
// MIDDLEWARE
// MIDDLEWARE RESPONSES
// throwUnauthorized - Throws a 403
func throwUnauthorized(w http.ResponseWriter, m string) {
jr := jsonResponse{
@ -149,6 +156,7 @@ func generateJsonError(m string) []byte {
return js
}
// MIDDLEWARE ACTIONS
// tokenMiddleware - Validates token to a user
func tokenMiddleware(next func(http.ResponseWriter, *http.Request, string)) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
@ -182,15 +190,11 @@ func jwtMiddleware(next func(http.ResponseWriter, *http.Request, string, string)
var reqUuid string
for k, v := range mux.Vars(r) {
if k == "id" {
if k == "uuid" {
reqUuid = v
}
}
if reqUuid == "" {
throwBadReq(w, "Invalid Request")
}
next(w, r, claims.Subject, reqUuid)
}
}
@ -311,13 +315,13 @@ func handleIngress(w http.ResponseWriter, r *http.Request, userUuid string) {
if err != nil {
// log.Printf("Error inserting track: %+v", err)
tx.Rollback()
throwBadReq(w, err.Error())
throwOkError(w, err.Error())
return
}
err = tx.Commit()
if err != nil {
throwBadReq(w, err.Error())
throwOkError(w, err.Error())
return
}
@ -328,11 +332,35 @@ func handleIngress(w http.ResponseWriter, r *http.Request, userUuid string) {
throwBadReq(w, "Unknown ingress type")
}
// fetchUser - Return personal userprofile
func fetchUser(w http.ResponseWriter, r *http.Request, jwtUser string, reqUser string) {
// We don't this var most of the time
userFull, err := getUser(jwtUser)
if err != nil {
throwOkError(w, "Failed to fetch user information")
return
}
jsonFull, _ := json.Marshal(&userFull)
// Lets strip out vars we don't want to send.
user := UserResponse{}
err = json.Unmarshal(jsonFull, &user)
if err != nil {
throwOkError(w, "Failed to fetch user information")
return
}
json, _ := json.Marshal(&user)
w.WriteHeader(http.StatusOK)
w.Write(json)
}
// fetchScrobbles - Return an array of scrobbles
func fetchScrobbleResponse(w http.ResponseWriter, r *http.Request, jwtUser string, reqUser string) {
resp, err := fetchScrobblesForUser(reqUser, 1)
resp, err := fetchScrobblesForUser(reqUser, 100, 1)
if err != nil {
throwBadReq(w, "Failed to fetch scrobbles")
throwOkError(w, "Failed to fetch scrobbles")
return
}
@ -374,6 +402,37 @@ func postConfig(w http.ResponseWriter, r *http.Request, jwtUser string) {
throwOkMessage(w, "Config updated successfully")
}
// fetchProfile - Returns public user profile data
func fetchProfile(w http.ResponseWriter, r *http.Request) {
var username string
for k, v := range mux.Vars(r) {
if k == "username" {
username = v
}
}
if username == "" {
throwOkError(w, "Invalid Username")
return
}
user, err := getUserByUsername(username)
if err != nil {
throwOkError(w, err.Error())
return
}
resp, err := getProfile(user)
if err != nil {
throwOkError(w, err.Error())
return
}
json, _ := json.Marshal(&resp)
w.WriteHeader(http.StatusOK)
w.Write(json)
}
// FRONTEND HANDLING
// ServerHTTP - Frontend server

View File

@ -3,11 +3,14 @@ package goscrobble
import (
"errors"
"math/rand"
"time"
)
const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
// generateToken - Generates a unique token for user input
func generateToken(n int) string {
rand.Seed(time.Now().UnixNano())
b := make([]byte, n)
for i := range b {
b[i] = letterBytes[rand.Int63()%int64(len(letterBytes))]

View File

@ -29,6 +29,17 @@ type User struct {
Admin bool `json:"admin"`
}
type UserResponse struct {
UUID string `json:"uuid"`
CreatedAt time.Time `json:"created_at"`
CreatedIp net.IP `json:"created_ip"`
ModifiedAt time.Time `json:"modified_at"`
ModifiedIP net.IP `jsos:"modified_ip"`
Username string `json:"username"`
Email string `json:"email"`
Verified bool `json:"verified"`
}
// RegisterRequest - Incoming JSON
type RegisterRequest struct {
Username string `json:"username"`
@ -205,3 +216,15 @@ func getUser(uuid string) (User, error) {
return user, nil
}
func getUserByUsername(username string) (User, error) {
var user User
err := db.QueryRow("SELECT BIN_TO_UUID(`uuid`, true), `created_at`, `created_ip`, `modified_at`, `modified_ip`, `username`, `email`, `password`, `verified`, `admin` FROM `users` WHERE `username` = ? AND `active` = 1",
username).Scan(&user.UUID, &user.CreatedAt, &user.CreatedIp, &user.ModifiedAt, &user.ModifiedIP, &user.Username, &user.Email, &user.Password, &user.Verified, &user.Admin)
if err == sql.ErrNoRows {
return user, errors.New("Invalid Username")
}
return user, nil
}

View File

@ -10,6 +10,7 @@ CREATE TABLE IF NOT EXISTS `users` (
`verified` TINYINT(1) NOT NULL DEFAULT 0,
`active` TINYINT(1) NOT NULL DEFAULT 1,
`admin` TINYINT(1) NOT NULL DEFAULT 0,
`private` TINYINT(1) NOT NULL DEFAULT 0,
KEY `usernameLookup` (`username`, `active`),
KEY `emailLookup` (`email`, `active`)
) DEFAULT CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci;

View File

@ -34,8 +34,10 @@ CREATE TABLE IF NOT EXISTS `scrobbles` (
`created_ip` VARBINARY(16) NULL DEFAULT NULL,
`user` BINARY(16) NOT NULL,
`track` BINARY(16) NOT NULL,
`source` VARCHAR(100) NOT NULL DEFAULT '',
KEY `userLookup` (`user`),
KEY `dateLookup` (`created_at`),
KEY `sourceLookup` (`source`),
FOREIGN KEY (track) REFERENCES tracks(uuid),
FOREIGN KEY (user) REFERENCES users(uuid)
) DEFAULT CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci;

View File

@ -0,0 +1 @@
DROP TABLE IF EXISTS `files`;

View File

@ -0,0 +1,7 @@
CREATE TABLE IF NOT EXISTS `files` (
`uuid` BINARY(16) PRIMARY KEY,
`path` VARCHAR(255) NOT NULL,
`filesize` INT NULL DEFAULT NULL,
`dimension` VARCHAR(4) NOT NULL,
KEY `dimensionLookup` (`uuid`, `dimension`)
) DEFAULT CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci;

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