From 2a271dcc0da7e91f46ed4e4cb95d46eb30ae0a19 Mon Sep 17 00:00:00 2001 From: Ryan Nixon Date: Sun, 8 Sep 2024 13:34:40 -0700 Subject: [PATCH] Add pinning support --- internal/data/session.go | 3 +- internal/server/about.go | 2 +- internal/server/game.go | 6 +- internal/server/hx.go | 54 ++++++++++++-- internal/server/index.go | 60 ++++++++++------ internal/server/server.go | 27 +++++-- internal/server/templates/game-body.gohtml | 69 ------------------ internal/server/templates/game.gohtml | 70 ++++++++++++++++++- internal/server/templates/games-all.gohtml | 39 +++++++++++ internal/server/templates/games-body.gohtml | 61 ---------------- internal/server/templates/games-pinned.gohtml | 42 +++++++++++ internal/server/templates/games-row.gohtml | 11 --- internal/server/templates/games.gohtml | 49 ++++++++++++- internal/server/user.go | 6 +- 14 files changed, 311 insertions(+), 188 deletions(-) delete mode 100644 internal/server/templates/game-body.gohtml create mode 100644 internal/server/templates/games-all.gohtml delete mode 100644 internal/server/templates/games-body.gohtml create mode 100644 internal/server/templates/games-pinned.gohtml delete mode 100644 internal/server/templates/games-row.gohtml diff --git a/internal/data/session.go b/internal/data/session.go index f757e74..e13bfff 100644 --- a/internal/data/session.go +++ b/internal/data/session.go @@ -6,7 +6,8 @@ import ( ) type Session struct { - SteamID string `json:"SteamID"` + Pinned []uint64 + SteamID string } const DefaultSessionExpiration = time.Hour * 24 * 90 diff --git a/internal/server/about.go b/internal/server/about.go index d3930c5..4007b50 100644 --- a/internal/server/about.go +++ b/internal/server/about.go @@ -9,7 +9,7 @@ type aboutBag struct { } func (s *Server) aboutHandler(resp http.ResponseWriter, req *http.Request) { - data := aboutBag{baseBag: newBag(req, "about")} + data := aboutBag{baseBag: s.newBag(req, "about")} renderHtml(resp, http.StatusOK, "about.gohtml", data) } diff --git a/internal/server/game.go b/internal/server/game.go index 0b36985..6086e3b 100644 --- a/internal/server/game.go +++ b/internal/server/game.go @@ -15,7 +15,7 @@ type gameBag struct { } func (s *Server) gameHandler(resp http.ResponseWriter, req *http.Request) { - bag := gameBag{baseBag: newBag(req, "game")} + bag := gameBag{baseBag: s.newBag(req, "game")} gameIDString := req.PathValue("id") gameID, _ := strconv.ParseUint(gameIDString, 10, 64) @@ -41,9 +41,5 @@ func (s *Server) gameHandler(resp http.ResponseWriter, req *http.Request) { } template := "game.gohtml" - if req.Header.Get("HX-Request") != "" { - template = "game-body.gohtml" - } - renderHtml(resp, http.StatusOK, template, bag) } diff --git a/internal/server/hx.go b/internal/server/hx.go index b384c5e..aab6f84 100644 --- a/internal/server/hx.go +++ b/internal/server/hx.go @@ -3,6 +3,7 @@ package server import ( "fmt" "net/http" + "slices" "strconv" "github.com/taiidani/achievements/internal/data" @@ -14,23 +15,62 @@ type hxGameRowBag struct { Achievements data.Achievements } -func (s *Server) hxGameRowHandler(resp http.ResponseWriter, req *http.Request) { - bag := hxGameRowBag{baseBag: newBag(req, "")} +func (s *Server) hxGameRowHandler(w http.ResponseWriter, r *http.Request) { + bag := hxGameRowBag{baseBag: s.newBag(r, "")} - gameIDString := req.URL.Query().Get("game-id") + gameIDString := r.PathValue("id") bag.GameID, _ = strconv.ParseUint(gameIDString, 10, 64) if bag.SteamID == "" { - errorResponse(resp, http.StatusBadRequest, fmt.Errorf("user ID is required")) + errorResponse(w, http.StatusBadRequest, fmt.Errorf("user ID is required")) return } - achievements, err := s.backend.GetAchievements(req.Context(), bag.SteamID, bag.GameID) + achievements, err := s.backend.GetAchievements(r.Context(), bag.SteamID, bag.GameID) if err != nil { - errorResponse(resp, http.StatusNotFound, err) + errorResponse(w, http.StatusNotFound, err) return } bag.Achievements = achievements - renderHtml(resp, http.StatusOK, "hx-achievement-progress.gohtml", bag) + renderHtml(w, http.StatusOK, "hx-achievement-progress.gohtml", bag) +} + +type hxGamePinBag struct { + baseBag + HasPinned bool + Games []indexBagGame +} + +func (s *Server) hxGamePinHandler(w http.ResponseWriter, r *http.Request) { + bag := hxGamePinBag{baseBag: s.newBag(r, "")} + gameIDString := r.PathValue("id") + gameID, _ := strconv.ParseUint(gameIDString, 10, 64) + + if gameID == 0 { + errorResponse(w, http.StatusNotFound, fmt.Errorf("game-id required for pinning")) + return + } + + switch r.Method { + case http.MethodPost: + if !slices.Contains(bag.Session.Pinned, gameID) { + bag.Session.Pinned = append(bag.Session.Pinned, gameID) + _ = s.backend.SetSession(r.Context(), bag.SessionKey, *bag.Session) + } + case http.MethodDelete: + if ix := slices.Index(bag.Session.Pinned, gameID); ix >= 0 { + bag.Session.Pinned = slices.Delete(bag.Session.Pinned, ix, ix+1) + _ = s.backend.SetSession(r.Context(), bag.SessionKey, *bag.Session) + } + } + + var err error + bag.Games, bag.HasPinned, err = s.loadGamesList(r.Context(), bag.SteamID, bag.baseBag) + if err != nil { + errorResponse(w, http.StatusNotFound, err) + return + } + + renderHtml(w, http.StatusOK, "games-pinned.gohtml", bag) } diff --git a/internal/server/index.go b/internal/server/index.go index 16badf5..b22692a 100644 --- a/internal/server/index.go +++ b/internal/server/index.go @@ -1,8 +1,10 @@ package server import ( + "context" "fmt" "net/http" + "slices" "sort" "github.com/taiidani/achievements/internal/data" @@ -10,15 +12,18 @@ import ( type indexBag struct { baseBag - User data.User - Games []struct { - data.Game - SteamID string - } + User data.User + HasPinned bool + Games []indexBagGame +} + +type indexBagGame struct { + data.Game + Pinned bool } func (s *Server) indexHandler(resp http.ResponseWriter, req *http.Request) { - bag := indexBag{baseBag: newBag(req, "home")} + bag := indexBag{baseBag: s.newBag(req, "home")} // If no user has been set, display the welcome page if bag.SteamID == "" { @@ -45,30 +50,41 @@ func (s *Server) indexHandler(resp http.ResponseWriter, req *http.Request) { } bag.User = user - games, err := s.backend.GetGames(req.Context(), bag.SteamID) + bag.Games, bag.HasPinned, err = s.loadGamesList(req.Context(), user.SteamID, bag.baseBag) if err != nil { errorResponse(resp, http.StatusNotFound, err) return } - for _, game := range games { - bag.Games = append(bag.Games, struct { - data.Game - SteamID string - }{ - Game: game, - SteamID: bag.SteamID, - }) + template := "games.gohtml" + renderHtml(resp, http.StatusOK, template, bag) +} + +func (s *Server) loadGamesList(ctx context.Context, steamID string, bag baseBag) ([]indexBagGame, bool, error) { + ret := []indexBagGame{} + retPinned := false + + games, err := s.backend.GetGames(ctx, steamID) + if err != nil { + return ret, false, err } - sort.Slice(bag.Games, func(i, j int) bool { - return bag.Games[i].DisplayName < bag.Games[j].DisplayName - }) + for _, game := range games { + bagGame := indexBagGame{ + Game: game, + Pinned: bag.Session != nil && slices.Contains(bag.Session.Pinned, game.ID), + } - template := "games.gohtml" - if req.Header.Get("HX-Request") != "" { - template = "games-body.gohtml" + if bagGame.Pinned { + retPinned = true + } + + ret = append(ret, bagGame) } - renderHtml(resp, http.StatusOK, template, bag) + sort.Slice(ret, func(i, j int) bool { + return ret[i].DisplayName < ret[j].DisplayName + }) + + return ret, retPinned, nil } diff --git a/internal/server/server.go b/internal/server/server.go index f961c59..2be21f1 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -57,7 +57,8 @@ func (s *Server) addRoutes(mux *http.ServeMux) { mux.Handle("/game/{id}", s.sessionMiddleware(http.HandlerFunc(s.gameHandler))) mux.Handle("/about", s.sessionMiddleware(http.HandlerFunc(s.aboutHandler))) mux.Handle("/assets/*", http.HandlerFunc(s.assetsHandler)) - mux.Handle("/hx/game/row", s.sessionMiddleware(http.HandlerFunc(s.hxGameRowHandler))) + mux.Handle("/hx/game/{id}/row", s.sessionMiddleware(http.HandlerFunc(s.hxGameRowHandler))) + mux.Handle("/hx/game/{id}/pin", s.sessionMiddleware(http.HandlerFunc(s.hxGamePinHandler))) mux.Handle("/user/login", s.sessionMiddleware(http.HandlerFunc(s.userLoginHandler))) mux.Handle("/user/login/steam", s.sessionMiddleware(http.HandlerFunc(s.userLoginSteamHandler))) mux.Handle("/user/change", s.sessionMiddleware(http.HandlerFunc(s.userChangeHandler))) @@ -88,15 +89,29 @@ func renderHtml(writer http.ResponseWriter, code int, file string, data any) { } type baseBag struct { - Page string - LoggedIn bool - SteamID string + SessionKey string + Session *data.Session + Page string + LoggedIn bool + SteamID string } -func newBag(r *http.Request, pageName string) baseBag { +func (s *Server) newBag(r *http.Request, pageName string) baseBag { ret := baseBag{} ret.Page = pageName - ret.LoggedIn = r.Header.Get(steamIDHeaderKey) != "" + + // Load the session if it exists + cookie, err := r.Cookie("session") + if err == nil { + ret.SessionKey = cookie.Value + sess, err := s.backend.GetSession(r.Context(), cookie.Value) + if err != nil { + slog.Warn("Unable to retrieve session", "key", cookie.Value, "error", err) + } else { + ret.Session = sess + ret.LoggedIn = true + } + } // Prioritize the query parameter over the session ID ret.SteamID = r.FormValue("steam-id") diff --git a/internal/server/templates/game-body.gohtml b/internal/server/templates/game-body.gohtml deleted file mode 100644 index dd3e489..0000000 --- a/internal/server/templates/game-body.gohtml +++ /dev/null @@ -1,69 +0,0 @@ -
-
- -
-
-
-
-
{{.Game.DisplayName}} Logo
-

{{.Game.DisplayName}} {{- if eq .Achievements.AchievementUnlockedPercentage 100 }} 🏆{{end}}

- {{ if .Achievements.Achievements }} -

-

-
{{ if gt .Achievements.AchievementUnlockedPercentage 10 }}{{ .Achievements.AchievementUnlockedPercentage }}% Complete{{ end }}
-
-

- {{ end }} - -
- {{ if not .Achievements.Achievements }} -

This game has no published achievements.

- {{ else }} - - - - - - - - - - - {{ range $i, $el := .Achievements.Achievements }} - - - - - - - {{ end }} - -
NameUnlocked OnGlobal Percentage
- {{$el.Name}} - -

{{$el.Name}}

- {{ if $el.Hidden }} -

Description intentionally hidden.

- {{ else if not $el.Description }} -

Description not found.

- {{ else }} -

{{$el.Description}}

- {{ end }} -
- {{ if $el.UnlockedOn }} -

✅ {{$el.UnlockedOn.Format "2006-01-02" }}

- {{ else }} -

- {{ end }} -
-

{{ printf "%.02f%%" $el.GlobalPercentage}}

-
- {{ end }} -
-
-
diff --git a/internal/server/templates/game.gohtml b/internal/server/templates/game.gohtml index 54be3c2..158f99a 100644 --- a/internal/server/templates/game.gohtml +++ b/internal/server/templates/game.gohtml @@ -1,7 +1,75 @@ {{ template "header.gohtml" . }}
- {{ template "game-body.gohtml" . }} +
+
+ +
+
+
+
+
{{.Game.DisplayName}} Logo
+

{{.Game.DisplayName}} {{- if eq .Achievements.AchievementUnlockedPercentage 100 }} 🏆{{end}}

+ {{ if .Achievements.Achievements }} +

+

+
{{ if gt .Achievements.AchievementUnlockedPercentage 10 }}{{ .Achievements.AchievementUnlockedPercentage }}% Complete{{ end }}
+
+

+ {{ end }} + +
+ {{ if not .Achievements.Achievements }} +

This game has no published achievements.

+ {{ else }} + + + + + + + + + + + {{ range $i, $el := .Achievements.Achievements }} + + + + + + + {{ end }} + +
NameUnlocked OnGlobal Percentage
+ {{$el.Name}} + +

{{$el.Name}}

+ {{ if $el.Hidden }} +

Description intentionally hidden.

+ {{ else if not $el.Description }} +

Description not found.

+ {{ else }} +

{{$el.Description}}

+ {{ end }} +
+ {{ if $el.UnlockedOn }} +

✅ {{$el.UnlockedOn.Format "2006-01-02" }}

+ {{ else }} +

+ {{ end }} +
+

{{ printf "%.02f%%" $el.GlobalPercentage}}

+
+ {{ end }} +
+
+
{{ template "footer.gohtml" . }} diff --git a/internal/server/templates/games-all.gohtml b/internal/server/templates/games-all.gohtml new file mode 100644 index 0000000..7804688 --- /dev/null +++ b/internal/server/templates/games-all.gohtml @@ -0,0 +1,39 @@ +{{- $steamID := .SteamID }} +{{- $loggedIn := .LoggedIn }} +

Games

+ + + + + + + + + + {{ if $loggedIn }} + + {{ end }} + + + + + {{ range .Games }} + + + + + + + {{ if $loggedIn }} + + {{ end }} + + {{ end }} + +
{{/* Logo */}}NameAchievement ProgressTime PlayedLast PlayedActions
+ {{.DisplayName}} Logo + + {{.DisplayName}} + + + {{ printf "%.00f" .PlaytimeForever.Hours }} hours{{ if .LastPlayed.IsZero }}Unknown{{ else }}{{ .LastPlayed.Format "2006-01-02" }}{{ end }}
diff --git a/internal/server/templates/games-body.gohtml b/internal/server/templates/games-body.gohtml deleted file mode 100644 index 24a549d..0000000 --- a/internal/server/templates/games-body.gohtml +++ /dev/null @@ -1,61 +0,0 @@ -
-
-
- -
-
- -
- {{ if .User.Name }} -
-
-
- {{ .User.Name }} -
-
-
-
{{ .User.Name }}
-

Last Online: {{ if .User.LastLogoff.IsZero }}Unknown{{ else }}{{ .User.LastLogoff.Format "2006-01-02" }}{{ end }}

-

-

- -
-

-
-
-
-
- {{ end }} -
- - {{ if .SteamID }} -
-
-

Games

- - - - - - - - - - - - - {{ range $i, $el := .Games }} - - {{ template "games-row.gohtml" $el }} - - {{ end }} - -
{{/* Logo */}}NameAchievement ProgressTime PlayedLast Played
-
-
- {{ end }} -
diff --git a/internal/server/templates/games-pinned.gohtml b/internal/server/templates/games-pinned.gohtml new file mode 100644 index 0000000..ee9607c --- /dev/null +++ b/internal/server/templates/games-pinned.gohtml @@ -0,0 +1,42 @@ +{{ if .HasPinned }} +{{- $steamID := .SteamID }} +{{- $loggedIn := .LoggedIn }} +

Pinned

+ +
+ {{ range .Games }} + {{ if .Pinned }} +
+
+ {{.DisplayName}} Logo +
+ +
+
+
{{.DisplayName}}
+ + + + + + {{ if $loggedIn }} + + {{ end }} + + + + + + + + + +
Time PlayedLast PlayedActions
{{ printf "%.00f" .PlaytimeForever.Hours }} hours{{ if .LastPlayed.IsZero }}Unknown{{ else }}{{ .LastPlayed.Format "2006-01-02" }}{{ end }}
+ {{/*

This is a longer card with supporting text below as a natural lead-in to additional content. This content is a little bit longer.

*/}} +
+
+
+ {{ end }} + {{ end }} +
+{{ end }} diff --git a/internal/server/templates/games-row.gohtml b/internal/server/templates/games-row.gohtml deleted file mode 100644 index 9b80e8a..0000000 --- a/internal/server/templates/games-row.gohtml +++ /dev/null @@ -1,11 +0,0 @@ - - {{.DisplayName}} Logo - - - {{$.DisplayName}} - - - - -{{ printf "%.00f" .PlaytimeForever.Hours }} hours -{{ if .LastPlayed.IsZero }}Unknown{{ else }}{{ .LastPlayed.Format "2006-01-02" }}{{ end }} diff --git a/internal/server/templates/games.gohtml b/internal/server/templates/games.gohtml index 5e6eba1..75e1404 100644 --- a/internal/server/templates/games.gohtml +++ b/internal/server/templates/games.gohtml @@ -1,7 +1,54 @@ {{ template "header.gohtml" . }}
- {{ template "games-body.gohtml" . }} +
+
+
+ +
+
+ +
+ {{ if .User.Name }} +
+
+
+
+ {{ .User.Name }} +
+
+
+
{{ .User.Name }}
+

Last Online: {{ if .User.LastLogoff.IsZero }}Unknown{{ else }}{{ .User.LastLogoff.Format "2006-01-02" }}{{ end }}

+

+

+ +
+

+
+
+
+
+
+ {{ end }} +
+ + {{ if .SteamID }} +
+
+ {{ template "games-pinned.gohtml" .}} +
+ +
+ {{ template "games-all.gohtml" .}} +
+
+ {{ end }} +
{{ template "footer.gohtml" . }} diff --git a/internal/server/user.go b/internal/server/user.go index 7385262..006dfa0 100644 --- a/internal/server/user.go +++ b/internal/server/user.go @@ -15,7 +15,7 @@ type userChangeBag struct { } func (s *Server) userChangeHandler(w http.ResponseWriter, r *http.Request) { - bag := userChangeBag{baseBag: newBag(r, "change-user")} + bag := userChangeBag{baseBag: s.newBag(r, "change-user")} template := "change-user.gohtml" renderHtml(w, http.StatusOK, template, bag) @@ -27,7 +27,7 @@ type userLoginBag struct { } func (s *Server) userLoginHandler(w http.ResponseWriter, r *http.Request) { - bag := userLoginBag{baseBag: newBag(r, "user-login")} + bag := userLoginBag{baseBag: s.newBag(r, "user-login")} // If the user is already logged in, redirect them to the homepage if bag.SteamID != "" { @@ -52,7 +52,7 @@ type userLoginSteamBag struct { } func (s *Server) userLoginSteamHandler(w http.ResponseWriter, r *http.Request) { - bag := userLoginSteamBag{baseBag: newBag(r, "user-login-steam")} + bag := userLoginSteamBag{baseBag: s.newBag(r, "user-login-steam")} // If the user is already logged in, redirect them to the homepage if bag.SteamID != "" {