Files
x0gp/server/cmd/poc-server/leaderboard_handlers.go
T

184 lines
5.7 KiB
Go

package main
import (
"net/http"
"strconv"
"github.com/x0gp/server/internal/races"
"github.com/x0gp/server/internal/transport"
)
// leaderboardHandler godoc
// @Summary Leaderboard
// @Description Returns paginated leaderboard list. Supports overall leaderboard or track-specific leaderboard for the current year.
// @Tags leaderboard
// @Produce json
// @Param track_id query string false "Track ID. If empty, returns overall leaderboard."
// @Param year query int false "Year. Defaults to current year."
// @Param limit query int false "Limit (1..200, default 50)"
// @Param offset query int false "Offset (default 0)"
// @Param X-Driver-Id header string false "Current driver ID. Defaults to driver-seed-001 (mock Alice)."
// @Success 200 {object} transport.LeaderboardResponse "Leaderboard page and current driver position"
// @Failure 400 {object} map[string]interface{} "Invalid input"
// @Failure 500 {object} map[string]interface{} "Internal server error"
// @Router /api/leaderboard [get]
func leaderboardHandler(svc *races.Service) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
trackID := r.URL.Query().Get("track_id")
yearVal := r.URL.Query().Get("year")
var year int
if yearVal != "" {
var err error
year, err = strconv.Atoi(yearVal)
if err != nil {
writeError(w, http.StatusBadRequest, "invalid_input", "invalid year")
return
}
}
limit := 50
limitVal := r.URL.Query().Get("limit")
if limitVal != "" {
var err error
limit, err = strconv.Atoi(limitVal)
if err != nil || limit <= 0 {
writeError(w, http.StatusBadRequest, "invalid_input", "invalid limit")
return
}
if limit > 200 {
limit = 200
}
}
offset := 0
offsetVal := r.URL.Query().Get("offset")
if offsetVal != "" {
var err error
offset, err = strconv.Atoi(offsetVal)
if err != nil || offset < 0 {
writeError(w, http.StatusBadRequest, "invalid_input", "invalid offset")
return
}
}
driverID := r.Header.Get("X-Driver-Id")
if driverID == "" {
driverID = "driver-seed-001" // mock current driver
}
res, err := svc.GetLeaderboard(r.Context(), trackID, year, limit, offset, driverID)
if err != nil {
writeError(w, http.StatusInternalServerError, "internal", err.Error())
return
}
items := make([]transport.LeaderboardEntry, 0, len(res.Items))
for _, item := range res.Items {
items = append(items, leaderboardEntryToWire(item))
}
var curDriver *transport.LeaderboardEntry
if res.CurrentDriver != nil {
wire := leaderboardEntryToWire(*res.CurrentDriver)
curDriver = &wire
}
writeJSON(w, http.StatusOK, transport.LeaderboardResponse{
Items: items,
Total: res.Total,
Limit: limit,
Offset: offset,
CurrentDriver: curDriver,
})
}
}
func leaderboardEntryToWire(e races.LeaderboardEntry) transport.LeaderboardEntry {
var bestPos *int
if e.BestPos != nil {
pos := *e.BestPos
bestPos = &pos
}
var bestTime *int64
if e.BestTimeMs != nil {
val := *e.BestTimeMs
bestTime = &val
}
return transport.LeaderboardEntry{
DriverID: e.DriverID,
Nickname: e.Nickname,
Name: e.Name,
AvatarURL: e.AvatarURL,
ClanID: e.ClanID,
ClanTag: e.ClanTag,
Points: e.Points,
BestPos: bestPos,
BestTimeMs: bestTime,
Rank: e.Rank,
}
}
// calendarLeaderboardHandler godoc
// @Summary Track Calendar Leaderboards
// @Description Returns the list of scheduled tracks with their status (finished/active/planned), top 3 drivers, and current driver position, sorted by calendar start date.
// @Tags leaderboard
// @Produce json
// @Param year query int false "Year. Defaults to current year."
// @Param X-Driver-Id header string false "Current driver ID. Defaults to driver-seed-001 (mock Alice)."
// @Success 200 {object} transport.TrackCalendarLeaderboardResponse "Calendar leaderboards list"
// @Failure 400 {object} map[string]interface{} "Invalid input"
// @Failure 500 {object} map[string]interface{} "Internal server error"
// @Router /api/leaderboard/tracks [get]
func calendarLeaderboardHandler(svc *races.Service) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
yearVal := r.URL.Query().Get("year")
var year int
if yearVal != "" {
var err error
year, err = strconv.Atoi(yearVal)
if err != nil {
writeError(w, http.StatusBadRequest, "invalid_input", "invalid year")
return
}
}
driverID := r.Header.Get("X-Driver-Id")
if driverID == "" {
driverID = "driver-seed-001" // mock current driver
}
res, err := svc.GetCalendarLeaderboard(r.Context(), year, driverID)
if err != nil {
writeError(w, http.StatusInternalServerError, "internal", err.Error())
return
}
items := make([]transport.TrackCalendarLeaderboard, 0, len(res))
for _, item := range res {
podium := make([]transport.LeaderboardEntry, 0, len(item.Podium))
for _, entry := range item.Podium {
podium = append(podium, leaderboardEntryToWire(entry))
}
var curDriver *transport.LeaderboardEntry
if item.CurrentDriver != nil {
wire := leaderboardEntryToWire(*item.CurrentDriver)
curDriver = &wire
}
items = append(items, transport.TrackCalendarLeaderboard{
TrackID: item.TrackID,
TrackName: item.TrackName,
StartAt: item.StartAt,
EndAt: item.EndAt,
Status: item.Status,
Podium: podium,
CurrentDriver: curDriver,
})
}
writeJSON(w, http.StatusOK, transport.TrackCalendarLeaderboardResponse{Items: items})
}
}