From e570314ac2934a1b1de11c0be8f6a89fd24715bc Mon Sep 17 00:00:00 2001 From: Daniel Mason Date: Thu, 1 Apr 2021 23:17:46 +1300 Subject: [PATCH] 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 --- .gitlab-ci.yml | 2 +- docs/changelog.md | 9 ++ internal/goscrobble/external_lastfm.go | 4 + .../{jellyfin.go => ingress_jellyfin.go} | 2 +- internal/goscrobble/profile.go | 21 +++++ internal/goscrobble/scrobble.go | 16 ++-- internal/goscrobble/server.go | 89 +++++++++++++++---- internal/goscrobble/tokens.go | 3 + internal/goscrobble/user.go | 23 +++++ migrations/2_users.up.sql | 1 + migrations/3_tracks.up.sql | 2 + migrations/temp/7_files.down.sql | 1 + migrations/temp/7_files.up.sql | 7 ++ web/public/index.html | 42 ++++++--- web/public/loader.svg | 52 +++++++++++ web/src/Api/index.js | 35 +++++++- web/src/App.css | 4 + web/src/App.js | 13 ++- web/src/Components/HomeBanner.js | 6 +- web/src/Components/Navigation.js | 25 +++--- web/src/Components/ScrobbleTable.js | 6 +- web/src/Pages/Admin.js | 26 +++--- web/src/Pages/Dashboard.js | 16 ++-- web/src/Pages/Profile.js | 56 +++++++++--- web/src/Pages/User.css | 4 + web/src/Pages/User.js | 53 +++++++++++ web/src/index.js | 6 +- 27 files changed, 435 insertions(+), 89 deletions(-) create mode 100644 internal/goscrobble/external_lastfm.go rename internal/goscrobble/{jellyfin.go => ingress_jellyfin.go} (96%) create mode 100644 internal/goscrobble/profile.go create mode 100644 migrations/temp/7_files.down.sql create mode 100644 migrations/temp/7_files.up.sql create mode 100644 web/public/loader.svg create mode 100644 web/src/Pages/User.css create mode 100644 web/src/Pages/User.js diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 886c4170..083d9d24 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -3,7 +3,7 @@ stages: - bundle variables: - VERSION: 0.0.8 + VERSION: 0.0.9 build-go: image: golang:1.16.2 diff --git a/docs/changelog.md b/docs/changelog.md index dbb4cd2e..8441ca50 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -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 diff --git a/internal/goscrobble/external_lastfm.go b/internal/goscrobble/external_lastfm.go new file mode 100644 index 00000000..5c810364 --- /dev/null +++ b/internal/goscrobble/external_lastfm.go @@ -0,0 +1,4 @@ +package goscrobble + +func getImageLastFM(src string) { +} diff --git a/internal/goscrobble/jellyfin.go b/internal/goscrobble/ingress_jellyfin.go similarity index 96% rename from internal/goscrobble/jellyfin.go rename to internal/goscrobble/ingress_jellyfin.go index 0c916c1e..4add3b6b 100644 --- a/internal/goscrobble/jellyfin.go +++ b/internal/goscrobble/ingress_jellyfin.go @@ -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") diff --git a/internal/goscrobble/profile.go b/internal/goscrobble/profile.go new file mode 100644 index 00000000..d8469b18 --- /dev/null +++ b/internal/goscrobble/profile.go @@ -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 +} diff --git a/internal/goscrobble/scrobble.go b/internal/goscrobble/scrobble.go index 9de9999c..37b1b920 100644 --- a/internal/goscrobble/scrobble.go +++ b/internal/goscrobble/scrobble.go @@ -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 } diff --git a/internal/goscrobble/server.go b/internal/goscrobble/server.go index d242682c..25e39c26 100644 --- a/internal/goscrobble/server.go +++ b/internal/goscrobble/server.go @@ -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 diff --git a/internal/goscrobble/tokens.go b/internal/goscrobble/tokens.go index e45cccba..35408fac 100644 --- a/internal/goscrobble/tokens.go +++ b/internal/goscrobble/tokens.go @@ -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))] diff --git a/internal/goscrobble/user.go b/internal/goscrobble/user.go index f889bf69..4c4bb54e 100644 --- a/internal/goscrobble/user.go +++ b/internal/goscrobble/user.go @@ -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 +} diff --git a/migrations/2_users.up.sql b/migrations/2_users.up.sql index 7203af66..0d85c2a0 100644 --- a/migrations/2_users.up.sql +++ b/migrations/2_users.up.sql @@ -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; \ No newline at end of file diff --git a/migrations/3_tracks.up.sql b/migrations/3_tracks.up.sql index 8219705e..ce0e1c76 100644 --- a/migrations/3_tracks.up.sql +++ b/migrations/3_tracks.up.sql @@ -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; diff --git a/migrations/temp/7_files.down.sql b/migrations/temp/7_files.down.sql new file mode 100644 index 00000000..fb06de9e --- /dev/null +++ b/migrations/temp/7_files.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS `files`; \ No newline at end of file diff --git a/migrations/temp/7_files.up.sql b/migrations/temp/7_files.up.sql new file mode 100644 index 00000000..065e3d6a --- /dev/null +++ b/migrations/temp/7_files.up.sql @@ -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; \ No newline at end of file diff --git a/web/public/index.html b/web/public/index.html index 6acbb8ff..1f0f7a7b 100644 --- a/web/public/index.html +++ b/web/public/index.html @@ -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`. --> + GoScrobble
- +
+
+
diff --git a/web/public/loader.svg b/web/public/loader.svg new file mode 100644 index 00000000..8bc9d6f0 --- /dev/null +++ b/web/public/loader.svg @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/web/src/Api/index.js b/web/src/Api/index.js index b4bab48c..382b055f 100644 --- a/web/src/Api/index.js +++ b/web/src/Api/index.js @@ -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 {}; + }); +}; diff --git a/web/src/App.css b/web/src/App.css index 44106f36..5616be07 100644 --- a/web/src/App.css +++ b/web/src/App.css @@ -1,3 +1,7 @@ +html, body { + background-color: #282c34; +} + .App { text-align: center; } diff --git a/web/src/App.js b/web/src/App.js index d4266fcd..e0e292f4 100644 --- a/web/src/App.js +++ b/web/src/App.js @@ -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 (
@@ -24,7 +30,8 @@ const App = () => { - + + diff --git a/web/src/Components/HomeBanner.js b/web/src/Components/HomeBanner.js index 66332e4a..376fdbcc 100644 --- a/web/src/Components/HomeBanner.js +++ b/web/src/Components/HomeBanner.js @@ -11,8 +11,10 @@ const HomeBanner = () => { useEffect(() => { getStats() .then(data => { - setBannerData(data); - setIsLoading(false); + if (data.users) { + setBannerData(data); + setIsLoading(false); + } }) }, []) diff --git a/web/src/Components/Navigation.js b/web/src/Components/Navigation.js index abc6dd4b..fdf6eb24 100644 --- a/web/src/Components/Navigation.js +++ b/web/src/Components/Navigation.js @@ -45,38 +45,41 @@ const Navigation = () => { {user ? : @@ -132,8 +137,8 @@ const Navigation = () => { {user ?
{user.username} {user.admin && diff --git a/web/src/Components/ScrobbleTable.js b/web/src/Components/ScrobbleTable.js index 57507072..3d62de58 100644 --- a/web/src/Components/ScrobbleTable.js +++ b/web/src/Components/ScrobbleTable.js @@ -14,9 +14,9 @@ const ScrobbleTable = (props) => { { - props.data && props.data.items && - props.data.items.map(function (element) { - return + props.data && + props.data.map(function (element) { + return {element.time} {element.track} {element.artist} diff --git a/web/src/Pages/Admin.js b/web/src/Pages/Admin.js index b492b913..1185169e 100644 --- a/web/src/Pages/Admin.js +++ b/web/src/Pages/Admin.js @@ -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 ( -
-

Unauthorized

-
- ) - } + const handleToggle = () => { + setToggle(!toggle); + }; if (loading) { return ( @@ -39,9 +37,13 @@ const Admin = () => { ) } - const handleToggle = () => { - setToggle(!toggle); - }; + if (!user || !user.admin) { + return ( +
+

Unauthorized

+
+ ) + } return (
diff --git a/web/src/Pages/Dashboard.js b/web/src/Pages/Dashboard.js index b5ad3a21..87503e2e 100644 --- a/web/src/Pages/Dashboard.js +++ b/web/src/Pages/Dashboard.js @@ -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 ( +
+ +
+ ) + } + return (

- Dashboard! + {user.username}'s Dashboard!

{loading ? - : + : }
); diff --git a/web/src/Pages/Profile.js b/web/src/Pages/Profile.js index 239f290d..aa1a3daf 100644 --- a/web/src/Pages/Profile.js +++ b/web/src/Pages/Profile.js @@ -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 ( +
+ +
+ ) + } + + if (!username || Object.keys(profile).length === 0) { + return ( +
+ Unable to fetch user +
+ ) } return (

- Welcome {user.username}! + {profile.username}'s Profile

+
+ Last 10 scrobbles...
+ +
); - } export default Profile; \ No newline at end of file diff --git a/web/src/Pages/User.css b/web/src/Pages/User.css new file mode 100644 index 00000000..3ed5eaa4 --- /dev/null +++ b/web/src/Pages/User.css @@ -0,0 +1,4 @@ +.userBody { + padding: 20px 5px 5px 5px; + font-size: 16pt; +} \ No newline at end of file diff --git a/web/src/Pages/User.js b/web/src/Pages/User.js new file mode 100644 index 00000000..4e8bf2b4 --- /dev/null +++ b/web/src/Pages/User.js @@ -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 ( +
+ +
+ ) + } + + if (!user) { + history.push("/login") + } + + return ( +
+

+ Welcome {userdata.username} +

+

+ Created At: {userdata.created_at}
+ Email: {userdata.email}
+ Verified: {userdata.verified ? '✓' : '✖'} +

+
+ ); +} + +export default User; \ No newline at end of file diff --git a/web/src/index.js b/web/src/index.js index 15dbe6a0..268225f2 100644 --- a/web/src/index.js +++ b/web/src/index.js @@ -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( - + - + , document.getElementById('root') );