mirror of
https://github.com/idanoo/GoScrobble.git
synced 2024-11-22 08:25:14 +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
@ -3,7 +3,7 @@ stages:
|
|||||||
- bundle
|
- bundle
|
||||||
|
|
||||||
variables:
|
variables:
|
||||||
VERSION: 0.0.8
|
VERSION: 0.0.9
|
||||||
|
|
||||||
build-go:
|
build-go:
|
||||||
image: golang:1.16.2
|
image: golang:1.16.2
|
||||||
|
@ -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
|
# 0.0.8
|
||||||
- Added Admin/Site config page in frontend for admin users
|
- Added Admin/Site config page in frontend for admin users
|
||||||
- Added API POST/GET /config endpoint
|
- Added API POST/GET /config endpoint
|
||||||
|
4
internal/goscrobble/external_lastfm.go
Normal file
4
internal/goscrobble/external_lastfm.go
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
package goscrobble
|
||||||
|
|
||||||
|
func getImageLastFM(src string) {
|
||||||
|
}
|
@ -50,7 +50,7 @@ func ParseJellyfinInput(userUUID string, data map[string]interface{}, ip net.IP,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Insert album if not exist
|
// Insert album if not exist
|
||||||
err = insertScrobble(userUUID, track.Uuid, ip, tx)
|
err = insertScrobble(userUUID, track.Uuid, "jellyfin", ip, tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("%+v", err)
|
log.Printf("%+v", err)
|
||||||
return errors.New("Failed to map track")
|
return errors.New("Failed to map track")
|
21
internal/goscrobble/profile.go
Normal file
21
internal/goscrobble/profile.go
Normal 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
|
||||||
|
}
|
@ -36,8 +36,8 @@ type ScrobbleRequestItem struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// insertScrobble - This will return if it exists or create it based on MBID > Name
|
// 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 {
|
func insertScrobble(user string, track string, source string, ip net.IP, tx *sql.Tx) error {
|
||||||
err := insertNewScrobble(user, track, ip, tx)
|
err := insertNewScrobble(user, track, source, ip, tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Error inserting scrobble %s %+v", user, err)
|
log.Printf("Error inserting scrobble %s %+v", user, err)
|
||||||
return errors.New("Failed to insert scrobble!")
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchScrobblesForUser(userUuid string, page int) (ScrobbleRequest, error) {
|
func fetchScrobblesForUser(userUuid string, limit int, page int) (ScrobbleRequest, error) {
|
||||||
scrobbleReq := ScrobbleRequest{}
|
scrobbleReq := ScrobbleRequest{}
|
||||||
var count int
|
var count int
|
||||||
|
|
||||||
@ -76,8 +76,8 @@ func fetchScrobblesForUser(userUuid string, page int) (ScrobbleRequest, error) {
|
|||||||
"JOIN albums ON track_album.album = albums.uuid "+
|
"JOIN albums ON track_album.album = albums.uuid "+
|
||||||
"JOIN users ON scrobbles.user = users.uuid "+
|
"JOIN users ON scrobbles.user = users.uuid "+
|
||||||
"WHERE user = UUID_TO_BIN(?, true) "+
|
"WHERE user = UUID_TO_BIN(?, true) "+
|
||||||
"ORDER BY scrobbles.created_at DESC LIMIT 500",
|
"ORDER BY scrobbles.created_at DESC LIMIT ?",
|
||||||
userUuid)
|
userUuid, limit)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed to fetch scrobbles: %+v", err)
|
log.Printf("Failed to fetch scrobbles: %+v", err)
|
||||||
return scrobbleReq, errors.New("Failed to fetch scrobbles")
|
return scrobbleReq, errors.New("Failed to fetch scrobbles")
|
||||||
@ -108,9 +108,9 @@ func fetchScrobblesForUser(userUuid string, page int) (ScrobbleRequest, error) {
|
|||||||
return scrobbleReq, nil
|
return scrobbleReq, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func insertNewScrobble(user string, track string, ip net.IP, tx *sql.Tx) error {
|
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`) "+
|
_, 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)
|
"VALUES (UUID_TO_BIN(UUID(), true), NOW(), ?, UUID_TO_BIN(?, true),UUID_TO_BIN(?, true), ?)", ip, user, track, source)
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -25,11 +25,14 @@ type jsonResponse struct {
|
|||||||
Msg string `json:"message,omitempty"`
|
Msg string `json:"message,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Limits to 1 req / 10 sec
|
// Limits to 1 req / 4 sec
|
||||||
var heavyLimiter = NewIPRateLimiter(0.25, 2)
|
var heavyLimiter = NewIPRateLimiter(0.25, 2)
|
||||||
|
|
||||||
// Limits to 5 req / sec
|
// 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
|
// List of Reverse proxies
|
||||||
var ReverseProxies []string
|
var ReverseProxies []string
|
||||||
@ -48,17 +51,21 @@ func HandleRequests(port string) {
|
|||||||
// Static Token for /ingress
|
// Static Token for /ingress
|
||||||
v1.HandleFunc("/ingress/jellyfin", tokenMiddleware(handleIngress)).Methods("POST")
|
v1.HandleFunc("/ingress/jellyfin", tokenMiddleware(handleIngress)).Methods("POST")
|
||||||
|
|
||||||
// JWT Auth
|
// JWT Auth - PWN PROFILE ONLY.
|
||||||
v1.HandleFunc("/user/{id}/scrobbles", jwtMiddleware(fetchScrobbleResponse)).Methods("GET")
|
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
|
// Config auth
|
||||||
v1.HandleFunc("/config", adminMiddleware(fetchConfig)).Methods("GET")
|
v1.HandleFunc("/config", adminMiddleware(fetchConfig)).Methods("GET")
|
||||||
v1.HandleFunc("/config", adminMiddleware(postConfig)).Methods("POST")
|
v1.HandleFunc("/config", adminMiddleware(postConfig)).Methods("POST")
|
||||||
|
|
||||||
// No Auth
|
// 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("/register", limitMiddleware(handleRegister, heavyLimiter)).Methods("POST")
|
||||||
v1.HandleFunc("/login", limitMiddleware(handleLogin, standardLimiter)).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
|
// This just prevents it serving frontend stuff over /api
|
||||||
r.PathPrefix("/api")
|
r.PathPrefix("/api")
|
||||||
@ -80,7 +87,7 @@ func HandleRequests(port string) {
|
|||||||
log.Fatal(http.ListenAndServe(":"+port, handler))
|
log.Fatal(http.ListenAndServe(":"+port, handler))
|
||||||
}
|
}
|
||||||
|
|
||||||
// MIDDLEWARE
|
// MIDDLEWARE RESPONSES
|
||||||
// throwUnauthorized - Throws a 403
|
// throwUnauthorized - Throws a 403
|
||||||
func throwUnauthorized(w http.ResponseWriter, m string) {
|
func throwUnauthorized(w http.ResponseWriter, m string) {
|
||||||
jr := jsonResponse{
|
jr := jsonResponse{
|
||||||
@ -149,6 +156,7 @@ func generateJsonError(m string) []byte {
|
|||||||
return js
|
return js
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MIDDLEWARE ACTIONS
|
||||||
// tokenMiddleware - Validates token to a user
|
// tokenMiddleware - Validates token to a user
|
||||||
func tokenMiddleware(next func(http.ResponseWriter, *http.Request, string)) http.HandlerFunc {
|
func tokenMiddleware(next func(http.ResponseWriter, *http.Request, string)) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
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
|
var reqUuid string
|
||||||
for k, v := range mux.Vars(r) {
|
for k, v := range mux.Vars(r) {
|
||||||
if k == "id" {
|
if k == "uuid" {
|
||||||
reqUuid = v
|
reqUuid = v
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if reqUuid == "" {
|
|
||||||
throwBadReq(w, "Invalid Request")
|
|
||||||
}
|
|
||||||
|
|
||||||
next(w, r, claims.Subject, reqUuid)
|
next(w, r, claims.Subject, reqUuid)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -311,13 +315,13 @@ func handleIngress(w http.ResponseWriter, r *http.Request, userUuid string) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
// log.Printf("Error inserting track: %+v", err)
|
// log.Printf("Error inserting track: %+v", err)
|
||||||
tx.Rollback()
|
tx.Rollback()
|
||||||
throwBadReq(w, err.Error())
|
throwOkError(w, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = tx.Commit()
|
err = tx.Commit()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
throwBadReq(w, err.Error())
|
throwOkError(w, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -328,11 +332,35 @@ func handleIngress(w http.ResponseWriter, r *http.Request, userUuid string) {
|
|||||||
throwBadReq(w, "Unknown ingress type")
|
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
|
// fetchScrobbles - Return an array of scrobbles
|
||||||
func fetchScrobbleResponse(w http.ResponseWriter, r *http.Request, jwtUser string, reqUser string) {
|
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 {
|
if err != nil {
|
||||||
throwBadReq(w, "Failed to fetch scrobbles")
|
throwOkError(w, "Failed to fetch scrobbles")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -374,6 +402,37 @@ func postConfig(w http.ResponseWriter, r *http.Request, jwtUser string) {
|
|||||||
throwOkMessage(w, "Config updated successfully")
|
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
|
// FRONTEND HANDLING
|
||||||
|
|
||||||
// ServerHTTP - Frontend server
|
// ServerHTTP - Frontend server
|
||||||
|
@ -3,11 +3,14 @@ package goscrobble
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||||
|
|
||||||
|
// generateToken - Generates a unique token for user input
|
||||||
func generateToken(n int) string {
|
func generateToken(n int) string {
|
||||||
|
rand.Seed(time.Now().UnixNano())
|
||||||
b := make([]byte, n)
|
b := make([]byte, n)
|
||||||
for i := range b {
|
for i := range b {
|
||||||
b[i] = letterBytes[rand.Int63()%int64(len(letterBytes))]
|
b[i] = letterBytes[rand.Int63()%int64(len(letterBytes))]
|
||||||
|
@ -29,6 +29,17 @@ type User struct {
|
|||||||
Admin bool `json:"admin"`
|
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
|
// RegisterRequest - Incoming JSON
|
||||||
type RegisterRequest struct {
|
type RegisterRequest struct {
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
@ -205,3 +216,15 @@ func getUser(uuid string) (User, error) {
|
|||||||
|
|
||||||
return user, nil
|
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
|
||||||
|
}
|
||||||
|
@ -10,6 +10,7 @@ CREATE TABLE IF NOT EXISTS `users` (
|
|||||||
`verified` TINYINT(1) NOT NULL DEFAULT 0,
|
`verified` TINYINT(1) NOT NULL DEFAULT 0,
|
||||||
`active` TINYINT(1) NOT NULL DEFAULT 1,
|
`active` TINYINT(1) NOT NULL DEFAULT 1,
|
||||||
`admin` TINYINT(1) NOT NULL DEFAULT 0,
|
`admin` TINYINT(1) NOT NULL DEFAULT 0,
|
||||||
|
`private` TINYINT(1) NOT NULL DEFAULT 0,
|
||||||
KEY `usernameLookup` (`username`, `active`),
|
KEY `usernameLookup` (`username`, `active`),
|
||||||
KEY `emailLookup` (`email`, `active`)
|
KEY `emailLookup` (`email`, `active`)
|
||||||
) DEFAULT CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci;
|
) DEFAULT CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci;
|
@ -34,8 +34,10 @@ CREATE TABLE IF NOT EXISTS `scrobbles` (
|
|||||||
`created_ip` VARBINARY(16) NULL DEFAULT NULL,
|
`created_ip` VARBINARY(16) NULL DEFAULT NULL,
|
||||||
`user` BINARY(16) NOT NULL,
|
`user` BINARY(16) NOT NULL,
|
||||||
`track` BINARY(16) NOT NULL,
|
`track` BINARY(16) NOT NULL,
|
||||||
|
`source` VARCHAR(100) NOT NULL DEFAULT '',
|
||||||
KEY `userLookup` (`user`),
|
KEY `userLookup` (`user`),
|
||||||
KEY `dateLookup` (`created_at`),
|
KEY `dateLookup` (`created_at`),
|
||||||
|
KEY `sourceLookup` (`source`),
|
||||||
FOREIGN KEY (track) REFERENCES tracks(uuid),
|
FOREIGN KEY (track) REFERENCES tracks(uuid),
|
||||||
FOREIGN KEY (user) REFERENCES users(uuid)
|
FOREIGN KEY (user) REFERENCES users(uuid)
|
||||||
) DEFAULT CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci;
|
) DEFAULT CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci;
|
||||||
|
1
migrations/temp/7_files.down.sql
Normal file
1
migrations/temp/7_files.down.sql
Normal file
@ -0,0 +1 @@
|
|||||||
|
DROP TABLE IF EXISTS `files`;
|
7
migrations/temp/7_files.up.sql
Normal file
7
migrations/temp/7_files.up.sql
Normal 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;
|
@ -24,20 +24,42 @@
|
|||||||
work correctly both with client-side routing and a non-root public URL.
|
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`.
|
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>
|
<title>GoScrobble</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
<!--
|
<div class="loader-container">
|
||||||
This HTML file is a template.
|
<div class="loader"></div>
|
||||||
If you open it directly in the browser, you will see an empty page.
|
</div>
|
||||||
|
|
||||||
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`.
|
|
||||||
-->
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</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) => {
|
export const PostLogin = (formValues) => {
|
||||||
// const { setLoading, setUser } = useContext(AuthContext);
|
|
||||||
// setLoading(true)
|
|
||||||
return axios.post(process.env.REACT_APP_API_URL + "login", formValues)
|
return axios.post(process.env.REACT_APP_API_URL + "login", formValues)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (response.data.token) {
|
if (response.data.token) {
|
||||||
@ -36,6 +34,7 @@ export const PostLogin = (formValues) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
|
toast.error('Failed to connect');
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@ -54,6 +53,7 @@ export const PostRegister = (formValues) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
|
toast.error('Failed to connect');
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@ -61,16 +61,21 @@ export const PostRegister = (formValues) => {
|
|||||||
export const getStats = () => {
|
export const getStats = () => {
|
||||||
return axios.get(process.env.REACT_APP_API_URL + "stats").then(
|
return axios.get(process.env.REACT_APP_API_URL + "stats").then(
|
||||||
(data) => {
|
(data) => {
|
||||||
data.isLoading = false;
|
|
||||||
return data.data;
|
return data.data;
|
||||||
}
|
}
|
||||||
);
|
).catch(() => {
|
||||||
|
toast.error('Failed to connect');
|
||||||
|
return {};
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getRecentScrobbles = (id) => {
|
export const getRecentScrobbles = (id) => {
|
||||||
return axios.get(process.env.REACT_APP_API_URL + "user/" + id + "/scrobbles", { headers: getHeaders() })
|
return axios.get(process.env.REACT_APP_API_URL + "user/" + id + "/scrobbles", { headers: getHeaders() })
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
return data.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() })
|
return axios.get(process.env.REACT_APP_API_URL + "config", { headers: getHeaders() })
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
return data.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 {
|
.App {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import { Route, Switch, withRouter } from 'react-router-dom';
|
import { Route, Switch, withRouter } from 'react-router-dom';
|
||||||
|
|
||||||
import Home from './Pages/Home';
|
import Home from './Pages/Home';
|
||||||
import About from './Pages/About';
|
import About from './Pages/About';
|
||||||
import Dashboard from './Pages/Dashboard';
|
import Dashboard from './Pages/Dashboard';
|
||||||
import Profile from './Pages/Profile';
|
import Profile from './Pages/Profile';
|
||||||
|
import User from './Pages/User';
|
||||||
import Admin from './Pages/Admin';
|
import Admin from './Pages/Admin';
|
||||||
import Login from './Pages/Login';
|
import Login from './Pages/Login';
|
||||||
import Register from './Pages/Register';
|
import Register from './Pages/Register';
|
||||||
@ -14,7 +14,13 @@ import 'bootstrap/dist/css/bootstrap.min.css';
|
|||||||
import './App.css';
|
import './App.css';
|
||||||
|
|
||||||
const App = () => {
|
const App = () => {
|
||||||
let boolTrue = true
|
let boolTrue = true;
|
||||||
|
|
||||||
|
// Remove loading spinner on load
|
||||||
|
const el = document.querySelector(".loader-container");
|
||||||
|
if (el) {
|
||||||
|
el.remove();
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@ -24,7 +30,8 @@ const App = () => {
|
|||||||
<Route path="/about" component={About} />
|
<Route path="/about" component={About} />
|
||||||
|
|
||||||
<Route path="/dashboard" component={Dashboard} />
|
<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} />
|
<Route path="/admin" component={Admin} />
|
||||||
|
|
||||||
|
@ -11,8 +11,10 @@ const HomeBanner = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getStats()
|
getStats()
|
||||||
.then(data => {
|
.then(data => {
|
||||||
setBannerData(data);
|
if (data.users) {
|
||||||
setIsLoading(false);
|
setBannerData(data);
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
@ -45,38 +45,41 @@ const Navigation = () => {
|
|||||||
{user ?
|
{user ?
|
||||||
<Nav className="navLinkLoginMobile" navbar>
|
<Nav className="navLinkLoginMobile" navbar>
|
||||||
{loggedInMenuItems.map(menuItem =>
|
{loggedInMenuItems.map(menuItem =>
|
||||||
<NavItem>
|
<NavItem key={menuItem}>
|
||||||
<Link
|
<Link
|
||||||
key={menuItem}
|
key={menuItem}
|
||||||
className="navLinkMobile"
|
className="navLinkMobile"
|
||||||
style={active === menuItem ? activeStyle : {}}
|
style={active === menuItem ? activeStyle : {}}
|
||||||
to={menuItem}
|
to={menuItem}
|
||||||
|
onClick={toggleCollapsed}
|
||||||
>{menuItem}</Link>
|
>{menuItem}</Link>
|
||||||
</NavItem>
|
</NavItem>
|
||||||
)}
|
)}
|
||||||
<Link
|
<Link
|
||||||
to="/profile"
|
to="/user"
|
||||||
style={active === "profile" ? activeStyle : {}}
|
style={active === "user" ? activeStyle : {}}
|
||||||
className="navLinkMobile"
|
className="navLinkMobile"
|
||||||
>Profile</Link>
|
onClick={toggleCollapsed}
|
||||||
|
>{user.username}</Link>
|
||||||
{user.admin &&
|
{user.admin &&
|
||||||
<Link
|
<Link
|
||||||
to="/admin"
|
to="/admin"
|
||||||
style={active === "admin" ? activeStyle : {}}
|
style={active === "admin" ? activeStyle : {}}
|
||||||
className="navLink"
|
className="navLink"
|
||||||
|
onClick={toggleCollapsed}
|
||||||
>Admin</Link>}
|
>Admin</Link>}
|
||||||
<Link to="/" className="navLink" onClick={Logout}>Logout</Link>
|
<Link to="/" className="navLink" onClick={Logout}>Logout</Link>
|
||||||
</Nav>
|
</Nav>
|
||||||
: <Nav className="navLinkLoginMobile" navbar>
|
: <Nav className="navLinkLoginMobile" navbar>
|
||||||
{menuItems.map(menuItem =>
|
{menuItems.map(menuItem =>
|
||||||
<NavItem>
|
<NavItem key={menuItem}>
|
||||||
<Link
|
<Link
|
||||||
key={menuItem}
|
key={menuItem}
|
||||||
className="navLinkMobile"
|
className="navLinkMobile"
|
||||||
style={active === menuItem ? activeStyle : {}}
|
style={active === menuItem ? activeStyle : {}}
|
||||||
to={menuItem === "Home" ? "/" : menuItem}
|
to={menuItem === "Home" ? "/" : menuItem}
|
||||||
>
|
onClick={toggleCollapsed}
|
||||||
{menuItem}
|
>{menuItem}
|
||||||
</Link>
|
</Link>
|
||||||
</NavItem>
|
</NavItem>
|
||||||
)}
|
)}
|
||||||
@ -85,6 +88,7 @@ const Navigation = () => {
|
|||||||
to="/Login"
|
to="/Login"
|
||||||
style={active === "Login" ? activeStyle : {}}
|
style={active === "Login" ? activeStyle : {}}
|
||||||
className="navLinkMobile"
|
className="navLinkMobile"
|
||||||
|
onClick={toggleCollapsed}
|
||||||
>Login</Link>
|
>Login</Link>
|
||||||
</NavItem>
|
</NavItem>
|
||||||
<NavItem>
|
<NavItem>
|
||||||
@ -92,6 +96,7 @@ const Navigation = () => {
|
|||||||
to="/Register"
|
to="/Register"
|
||||||
className="navLinkMobile"
|
className="navLinkMobile"
|
||||||
style={active === "Register" ? activeStyle : {}}
|
style={active === "Register" ? activeStyle : {}}
|
||||||
|
onClick={toggleCollapsed}
|
||||||
>Register</Link>
|
>Register</Link>
|
||||||
</NavItem>
|
</NavItem>
|
||||||
</Nav>
|
</Nav>
|
||||||
@ -132,8 +137,8 @@ const Navigation = () => {
|
|||||||
{user ?
|
{user ?
|
||||||
<div className="navLinkLogin">
|
<div className="navLinkLogin">
|
||||||
<Link
|
<Link
|
||||||
to="/profile"
|
to="/user"
|
||||||
style={active === "profile" ? activeStyle : {}}
|
style={active === "user" ? activeStyle : {}}
|
||||||
className="navLink"
|
className="navLink"
|
||||||
>{user.username}</Link>
|
>{user.username}</Link>
|
||||||
{user.admin &&
|
{user.admin &&
|
||||||
|
@ -14,9 +14,9 @@ const ScrobbleTable = (props) => {
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{
|
{
|
||||||
props.data && props.data.items &&
|
props.data &&
|
||||||
props.data.items.map(function (element) {
|
props.data.map(function (element) {
|
||||||
return <tr>
|
return <tr key={element.uuid}>
|
||||||
<td>{element.time}</td>
|
<td>{element.time}</td>
|
||||||
<td>{element.track}</td>
|
<td>{element.track}</td>
|
||||||
<td>{element.artist}</td>
|
<td>{element.artist}</td>
|
||||||
|
@ -17,19 +17,17 @@ const Admin = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getConfigs()
|
getConfigs()
|
||||||
.then(data => {
|
.then(data => {
|
||||||
setConfigs(data.configs);
|
if (data.configs) {
|
||||||
setToggle(data.configs.REGISTRATION_ENABLED === "1")
|
setConfigs(data.configs);
|
||||||
|
setToggle(data.configs.REGISTRATION_ENABLED === "1")
|
||||||
|
}
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
})
|
})
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
if (!user || !user.admin) {
|
const handleToggle = () => {
|
||||||
return (
|
setToggle(!toggle);
|
||||||
<div className="pageWrapper">
|
};
|
||||||
<h1>Unauthorized</h1>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
@ -39,9 +37,13 @@ const Admin = () => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleToggle = () => {
|
if (!user || !user.admin) {
|
||||||
setToggle(!toggle);
|
return (
|
||||||
};
|
<div className="pageWrapper">
|
||||||
|
<h1>Unauthorized</h1>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="pageWrapper">
|
<div className="pageWrapper">
|
||||||
|
@ -13,10 +13,6 @@ const Dashboard = () => {
|
|||||||
let [loading, setLoading] = useState(true);
|
let [loading, setLoading] = useState(true);
|
||||||
let [dashboardData, setDashboardData] = useState({});
|
let [dashboardData, setDashboardData] = useState({});
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
history.push("/login");
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return
|
return
|
||||||
@ -28,14 +24,22 @@ const Dashboard = () => {
|
|||||||
})
|
})
|
||||||
}, [user])
|
}, [user])
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="pageWrapper">
|
||||||
|
<ScaleLoader color="#6AD7E5" />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="pageWrapper">
|
<div className="pageWrapper">
|
||||||
<h1>
|
<h1>
|
||||||
Dashboard!
|
{user.username}'s Dashboard!
|
||||||
</h1>
|
</h1>
|
||||||
{loading
|
{loading
|
||||||
? <ScaleLoader color="#6AD7E5" size={60} />
|
? <ScaleLoader color="#6AD7E5" size={60} />
|
||||||
: <ScrobbleTable data={dashboardData} />
|
: <ScrobbleTable data={dashboardData.items} />
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -1,25 +1,59 @@
|
|||||||
import React, { useContext } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import '../App.css';
|
import '../App.css';
|
||||||
import './Dashboard.css';
|
import './Profile.css';
|
||||||
import { useHistory } from "react-router";
|
import ScaleLoader from 'react-spinners/ScaleLoader';
|
||||||
import AuthContext from '../Contexts/AuthContext';
|
import { getProfile } from '../Api/index'
|
||||||
|
import ScrobbleTable from '../Components/ScrobbleTable'
|
||||||
|
|
||||||
const Profile = () => {
|
const Profile = (route) => {
|
||||||
const history = useHistory();
|
const [loading, setLoading] = useState(true);
|
||||||
const { user } = useContext(AuthContext);
|
const [profile, setProfile] = useState({});
|
||||||
|
|
||||||
if (!user) {
|
let username = false;
|
||||||
history.push("/login");
|
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 (
|
return (
|
||||||
<div className="pageWrapper">
|
<div className="pageWrapper">
|
||||||
<h1>
|
<h1>
|
||||||
Welcome {user.username}!
|
{profile.username}'s Profile
|
||||||
</h1>
|
</h1>
|
||||||
|
<div className="profileBody">
|
||||||
|
Last 10 scrobbles...<br/>
|
||||||
|
<ScrobbleTable data={profile.scrobbles}/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Profile;
|
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 ReactDOM from 'react-dom';
|
||||||
import './index.css';
|
import './index.css';
|
||||||
import App from './App';
|
import App from './App';
|
||||||
import { HashRouter } from 'react-router-dom';
|
import { BrowserRouter } from 'react-router-dom';
|
||||||
|
|
||||||
import { ToastContainer } from 'react-toastify';
|
import { ToastContainer } from 'react-toastify';
|
||||||
import 'react-toastify/dist/ReactToastify.min.css';
|
import 'react-toastify/dist/ReactToastify.min.css';
|
||||||
@ -11,7 +11,7 @@ import AuthContextProvider from './Contexts/AuthContextProvider';
|
|||||||
|
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<AuthContextProvider>
|
<AuthContextProvider>
|
||||||
<HashRouter>
|
<BrowserRouter>
|
||||||
<ToastContainer
|
<ToastContainer
|
||||||
position="bottom-right"
|
position="bottom-right"
|
||||||
autoClose={5000}
|
autoClose={5000}
|
||||||
@ -24,7 +24,7 @@ ReactDOM.render(
|
|||||||
pauseOnHover
|
pauseOnHover
|
||||||
/>
|
/>
|
||||||
<App />
|
<App />
|
||||||
</HashRouter>
|
</BrowserRouter>
|
||||||
</AuthContextProvider>,
|
</AuthContextProvider>,
|
||||||
document.getElementById('root')
|
document.getElementById('root')
|
||||||
);
|
);
|
||||||
|
Loading…
Reference in New Issue
Block a user