mirror of https://github.com/muety/wakapi.git
feat: leaderboard aggregation functionality
feat: leaderboard ui design
This commit is contained in:
parent
1d7ff4bc2a
commit
a27fe04919
|
@ -1,6 +1,11 @@
|
|||
package models
|
||||
|
||||
import "time"
|
||||
import (
|
||||
"github.com/duke-git/lancet/v2/maputil"
|
||||
"github.com/duke-git/lancet/v2/slice"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type LeaderboardItem struct {
|
||||
ID uint `json:"-" gorm:"primary_key; size:32"`
|
||||
|
@ -13,3 +18,53 @@ type LeaderboardItem struct {
|
|||
Key *string `json:"key" gorm:"size:255"` // pointer because nullable
|
||||
CreatedAt CustomTime `gorm:"type:timestamp; default:CURRENT_TIMESTAMP" swaggertype:"string" format:"date" example:"2006-01-02 15:04:05.000"`
|
||||
}
|
||||
|
||||
type Leaderboard []*LeaderboardItem
|
||||
|
||||
func (l Leaderboard) UserIDs() []string {
|
||||
return slice.Unique[string](slice.Map[*LeaderboardItem, string](l, func(i int, item *LeaderboardItem) string {
|
||||
return item.UserID
|
||||
}))
|
||||
}
|
||||
|
||||
func (l Leaderboard) TopByKey(by uint8, key string) Leaderboard {
|
||||
return slice.Filter[*LeaderboardItem](l, func(i int, item *LeaderboardItem) bool {
|
||||
return item.By != nil && *item.By == by && item.Key != nil && strings.ToLower(*item.Key) == strings.ToLower(key)
|
||||
})
|
||||
}
|
||||
|
||||
func (l Leaderboard) TopKeys(by uint8) []string {
|
||||
type keyTotal struct {
|
||||
Key string
|
||||
Total time.Duration
|
||||
}
|
||||
|
||||
totalsMapped := make(map[string]*keyTotal, len(l))
|
||||
|
||||
for _, item := range l {
|
||||
if item.Key == nil || item.By == nil || *item.By != by {
|
||||
continue
|
||||
}
|
||||
if _, ok := totalsMapped[*item.Key]; !ok {
|
||||
totalsMapped[*item.Key] = &keyTotal{Key: *item.Key, Total: 0}
|
||||
}
|
||||
totalsMapped[*item.Key].Total += item.Total
|
||||
}
|
||||
|
||||
totals := slice.Map[*keyTotal, keyTotal](maputil.Values[string, *keyTotal](totalsMapped), func(i int, item *keyTotal) keyTotal {
|
||||
return *item
|
||||
})
|
||||
if err := slice.SortByField(totals, "Total", "desc"); err != nil {
|
||||
return []string{} // TODO
|
||||
}
|
||||
|
||||
return slice.Map[keyTotal, string](totals, func(i int, item keyTotal) string {
|
||||
return item.Key
|
||||
})
|
||||
}
|
||||
|
||||
func (l Leaderboard) TopKeysByUser(by uint8, userId string) []string {
|
||||
return Leaderboard(slice.Filter[*LeaderboardItem](l, func(i int, item *LeaderboardItem) bool {
|
||||
return item.UserID == userId
|
||||
})).TopKeys(by)
|
||||
}
|
||||
|
|
|
@ -5,7 +5,9 @@ import "github.com/muety/wakapi/models"
|
|||
type LeaderboardViewModel struct {
|
||||
User *models.User
|
||||
By string
|
||||
Key string
|
||||
Items []*models.LeaderboardItem
|
||||
TopKeys []string
|
||||
ApiKey string
|
||||
Success string
|
||||
Error string
|
||||
|
@ -20,3 +22,19 @@ func (s *LeaderboardViewModel) WithError(m string) *LeaderboardViewModel {
|
|||
s.Error = m
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *LeaderboardViewModel) ColorModifier(item *models.LeaderboardItem, principal *models.User) string {
|
||||
if principal != nil && item.UserID == principal.ID {
|
||||
return "self"
|
||||
}
|
||||
if item.Rank == 1 {
|
||||
return "gold"
|
||||
}
|
||||
if item.Rank == 2 {
|
||||
return "silver"
|
||||
}
|
||||
if item.Rank == 3 {
|
||||
return "bronze"
|
||||
}
|
||||
return "default"
|
||||
}
|
||||
|
|
|
@ -75,6 +75,7 @@ type IUserRepository interface {
|
|||
GetByEmail(string) (*models.User, error)
|
||||
GetByResetToken(string) (*models.User, error)
|
||||
GetAll() ([]*models.User, error)
|
||||
GetMany([]string) ([]*models.User, error)
|
||||
GetAllByReports(bool) ([]*models.User, error)
|
||||
GetAllByLeaderboard(bool) ([]*models.User, error)
|
||||
GetByLoggedInAfter(time.Time) ([]*models.User, error)
|
||||
|
|
|
@ -77,6 +77,17 @@ func (r *UserRepository) GetAll() ([]*models.User, error) {
|
|||
return users, nil
|
||||
}
|
||||
|
||||
func (r *UserRepository) GetMany(ids []string) ([]*models.User, error) {
|
||||
var users []*models.User
|
||||
if err := r.db.
|
||||
Table("users").
|
||||
Where("id in ?", ids).
|
||||
Find(&users).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return users, nil
|
||||
}
|
||||
|
||||
func (r *UserRepository) GetAllByReports(reportsEnabled bool) ([]*models.User, error) {
|
||||
var users []*models.User
|
||||
if err := r.db.Where(&models.User{ReportsWeekly: reportsEnabled}).Find(&users).Error; err != nil {
|
||||
|
|
|
@ -2,6 +2,7 @@ package routes
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/emvi/logbuch"
|
||||
"github.com/gorilla/mux"
|
||||
conf "github.com/muety/wakapi/config"
|
||||
"github.com/muety/wakapi/middlewares"
|
||||
|
@ -45,29 +46,41 @@ func (h *LeaderboardHandler) GetIndex(w http.ResponseWriter, r *http.Request) {
|
|||
if h.config.IsDev() {
|
||||
loadTemplates()
|
||||
}
|
||||
templates[conf.LeaderboardTemplate].Execute(w, h.buildViewModel(r))
|
||||
if err := templates[conf.LeaderboardTemplate].Execute(w, h.buildViewModel(r)); err != nil {
|
||||
logbuch.Error(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func (h *LeaderboardHandler) buildViewModel(r *http.Request) *view.LeaderboardViewModel {
|
||||
user := middlewares.GetPrincipal(r)
|
||||
byParam := strings.ToLower(r.URL.Query().Get("by"))
|
||||
keyParam := strings.ToLower(r.URL.Query().Get("key"))
|
||||
|
||||
var err error
|
||||
var items []*models.LeaderboardItem
|
||||
var leaderboard models.Leaderboard
|
||||
var topKeys []string
|
||||
|
||||
if byParam == "" {
|
||||
items, err = h.leaderboardService.GetByInterval(models.IntervalPast7Days)
|
||||
leaderboard, err = h.leaderboardService.GetByInterval(models.IntervalPast7Days, true)
|
||||
if err != nil {
|
||||
conf.Log().Request(r).Error("error while fetching general leaderboard items - %v", err)
|
||||
return &view.LeaderboardViewModel{Error: criticalError}
|
||||
}
|
||||
} else {
|
||||
if by, ok := allowedAggregations[byParam]; ok {
|
||||
items, err = h.leaderboardService.GetAggregatedByInterval(models.IntervalPast7Days, &by)
|
||||
leaderboard, err = h.leaderboardService.GetAggregatedByInterval(models.IntervalPast7Days, &by, true)
|
||||
if err != nil {
|
||||
conf.Log().Request(r).Error("error while fetching general leaderboard items - %v", err)
|
||||
return &view.LeaderboardViewModel{Error: criticalError}
|
||||
}
|
||||
|
||||
topKeys = leaderboard.TopKeys(by)
|
||||
if len(topKeys) > 0 {
|
||||
if keyParam == "" {
|
||||
keyParam = strings.ToLower(topKeys[0])
|
||||
}
|
||||
leaderboard = leaderboard.TopByKey(by, keyParam)
|
||||
}
|
||||
} else {
|
||||
return &view.LeaderboardViewModel{Error: fmt.Sprintf("unsupported aggregation '%s'", byParam)}
|
||||
}
|
||||
|
@ -81,7 +94,9 @@ func (h *LeaderboardHandler) buildViewModel(r *http.Request) *view.LeaderboardVi
|
|||
return &view.LeaderboardViewModel{
|
||||
User: user,
|
||||
By: byParam,
|
||||
Items: items,
|
||||
Key: keyParam,
|
||||
Items: leaderboard,
|
||||
TopKeys: topKeys,
|
||||
ApiKey: apiKey,
|
||||
Success: r.URL.Query().Get("success"),
|
||||
Error: r.URL.Query().Get("error"),
|
||||
|
|
|
@ -35,9 +35,11 @@ func DefaultTemplateFuncs() template.FuncMap {
|
|||
"join": strings.Join,
|
||||
"add": utils.Add,
|
||||
"capitalize": utils.Capitalize,
|
||||
"lower": strings.ToLower,
|
||||
"toRunes": utils.ToRunes,
|
||||
"localTZOffset": utils.LocalTZOffset,
|
||||
"entityTypes": models.SummaryTypes,
|
||||
"strslice": utils.SubSlice[string],
|
||||
"typeName": typeName,
|
||||
"isDev": func() bool {
|
||||
return config.Get().IsDev()
|
||||
|
|
|
@ -120,11 +120,11 @@ func (srv *LeaderboardService) ExistsAnyByUser(userId string) (bool, error) {
|
|||
return count > 0, err
|
||||
}
|
||||
|
||||
func (srv *LeaderboardService) GetByInterval(interval *models.IntervalKey) ([]*models.LeaderboardItem, error) {
|
||||
return srv.GetAggregatedByInterval(interval, nil)
|
||||
func (srv *LeaderboardService) GetByInterval(interval *models.IntervalKey, resolveUsers bool) (models.Leaderboard, error) {
|
||||
return srv.GetAggregatedByInterval(interval, nil, resolveUsers)
|
||||
}
|
||||
|
||||
func (srv *LeaderboardService) GetAggregatedByInterval(interval *models.IntervalKey, by *uint8) ([]*models.LeaderboardItem, error) {
|
||||
func (srv *LeaderboardService) GetAggregatedByInterval(interval *models.IntervalKey, by *uint8, resolveUsers bool) (models.Leaderboard, error) {
|
||||
// check cache
|
||||
cacheKey := srv.getHash(interval, by)
|
||||
if cacheResult, ok := srv.cache.Get(cacheKey); ok {
|
||||
|
@ -136,6 +136,21 @@ func (srv *LeaderboardService) GetAggregatedByInterval(interval *models.Interval
|
|||
return nil, err
|
||||
}
|
||||
|
||||
if resolveUsers {
|
||||
a := models.Leaderboard(items).UserIDs()
|
||||
println(a)
|
||||
users, err := srv.userService.GetManyMapped(models.Leaderboard(items).UserIDs())
|
||||
if err != nil {
|
||||
config.Log().Error("failed to resolve users for leaderboard item - %v", err)
|
||||
} else {
|
||||
for _, item := range items {
|
||||
if u, ok := users[item.UserID]; ok {
|
||||
item.User = u
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
srv.cache.SetDefault(cacheKey, items)
|
||||
return items, nil
|
||||
}
|
||||
|
|
|
@ -101,8 +101,8 @@ type ILeaderboardService interface {
|
|||
ScheduleDefault()
|
||||
Run([]*models.User, *models.IntervalKey, []uint8) error
|
||||
ExistsAnyByUser(string) (bool, error)
|
||||
GetByInterval(*models.IntervalKey) ([]*models.LeaderboardItem, error)
|
||||
GetAggregatedByInterval(*models.IntervalKey, *uint8) ([]*models.LeaderboardItem, error)
|
||||
GetByInterval(*models.IntervalKey, bool) (models.Leaderboard, error)
|
||||
GetAggregatedByInterval(*models.IntervalKey, *uint8, bool) (models.Leaderboard, error)
|
||||
GenerateByUser(*models.User, *models.IntervalKey) (*models.LeaderboardItem, error)
|
||||
GenerateAggregatedByUser(*models.User, *models.IntervalKey, uint8) ([]*models.LeaderboardItem, error)
|
||||
}
|
||||
|
@ -113,6 +113,8 @@ type IUserService interface {
|
|||
GetUserByEmail(string) (*models.User, error)
|
||||
GetUserByResetToken(string) (*models.User, error)
|
||||
GetAll() ([]*models.User, error)
|
||||
GetMany([]string) ([]*models.User, error)
|
||||
GetManyMapped([]string) (map[string]*models.User, error)
|
||||
GetAllByReports(bool) ([]*models.User, error)
|
||||
GetAllByLeaderboard(bool) ([]*models.User, error)
|
||||
GetActive(bool) ([]*models.User, error)
|
||||
|
|
|
@ -2,6 +2,7 @@ package services
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/duke-git/lancet/v2/convertor"
|
||||
"github.com/duke-git/lancet/v2/datetime"
|
||||
"github.com/emvi/logbuch"
|
||||
"github.com/leandro-lugaresi/hub"
|
||||
|
@ -96,6 +97,20 @@ func (srv *UserService) GetAll() ([]*models.User, error) {
|
|||
return srv.repository.GetAll()
|
||||
}
|
||||
|
||||
func (srv *UserService) GetMany(ids []string) ([]*models.User, error) {
|
||||
return srv.repository.GetMany(ids)
|
||||
}
|
||||
|
||||
func (srv *UserService) GetManyMapped(ids []string) (map[string]*models.User, error) {
|
||||
users, err := srv.repository.GetMany(ids)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return convertor.ToMap[*models.User, string, *models.User](users, func(u *models.User) (string, *models.User) {
|
||||
return u.ID, u
|
||||
}), nil
|
||||
}
|
||||
|
||||
func (srv *UserService) GetAllByReports(reportsEnabled bool) ([]*models.User, error) {
|
||||
return srv.repository.GetAllByReports(reportsEnabled)
|
||||
}
|
||||
|
|
|
@ -69,6 +69,10 @@ body {
|
|||
@apply py-2 px-4 font-semibold rounded bg-red-600 hover:bg-red-700 text-white text-sm;
|
||||
}
|
||||
|
||||
.btn-small {
|
||||
@apply py-1 px-2;
|
||||
}
|
||||
|
||||
.input-default {
|
||||
@apply appearance-none bg-gray-850 focus:bg-gray-800 text-gray-300 outline-none rounded w-full py-2 px-4;
|
||||
}
|
||||
|
@ -109,6 +113,30 @@ body {
|
|||
@apply border-red-700;
|
||||
}
|
||||
|
||||
.leaderboard-default {
|
||||
@apply border-gray-700;
|
||||
}
|
||||
|
||||
.leaderboard-self {
|
||||
margin-left: -10px;
|
||||
margin-right: -10px;
|
||||
padding-left: calc(1rem + 10px);
|
||||
padding-right: calc(1rem + 10px);
|
||||
@apply border-green-700 bg-gray-800;
|
||||
}
|
||||
|
||||
.leaderboard-gold {
|
||||
border-color: #ffd700;
|
||||
}
|
||||
|
||||
.leaderboard-silver {
|
||||
border-color: #c0c0c0;
|
||||
}
|
||||
|
||||
.leaderboard-bronze {
|
||||
border-color: #cd7f32;
|
||||
}
|
||||
|
||||
::-webkit-calendar-picker-indicator {
|
||||
filter: invert(1);
|
||||
cursor: pointer;
|
||||
|
|
File diff suppressed because one or more lines are too long
Binary file not shown.
|
@ -15,7 +15,7 @@ Iconify.addCollection({"prefix":"akar-icons","icons":{"chevron-down":{"body":"<g
|
|||
Iconify.addCollection({"prefix":"ls","icons":{"logout":{"body":"<path d=\"M0 151v391c0 36 15 68 39 93c24 24 55 37 91 37h196v-81H130c-27 0-48-22-48-49V151c0-27 21-48 48-48h196V21H130c-36 0-67 14-91 38c-24 25-39 56-39 92zm215 118v156c0 18 16 33 34 33h181v123c0 11 6 20 16 25c4 1 8 1 10 1c7 0 13-2 18-7l235-235c11-9 10-27 0-37L474 94c-14-15-44-6-44 18v124H249c-18 0-34 15-34 33z\" fill=\"currentColor\"/>","height":672}},"width":717,"height":717,"inlineHeight":1086,"inlineTop":-205,"verticalAlign":-0.2});
|
||||
Iconify.addCollection({"prefix":"majesticons","icons":{"clipboard-copy":{"body":"<g fill=\"none\"><path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M10 4a1 1 0 0 0 0 2h4a1 1 0 0 0 0-2h-4zm0-2a3.001 3.001 0 0 0-2.83 2H6a3 3 0 0 0-3 3v12a3 3 0 0 0 3 3h12a3 3 0 0 0 3-3v-4h-8.586l1.293 1.293a1 1 0 0 1-1.414 1.414l-3-3a1 1 0 0 1 0-1.414l3-3a1 1 0 1 1 1.414 1.414L12.414 13H21V7a3 3 0 0 0-3-3h-1.17A3.001 3.001 0 0 0 14 2h-4z\" fill=\"currentColor\"/></g>"}},"width":24,"height":24});
|
||||
Iconify.addCollection({"prefix":"fa-regular","icons":{"calendar-alt":{"body":"<path d=\"M148 288h-40c-6.6 0-12-5.4-12-12v-40c0-6.6 5.4-12 12-12h40c6.6 0 12 5.4 12 12v40c0 6.6-5.4 12-12 12zm108-12v-40c0-6.6-5.4-12-12-12h-40c-6.6 0-12 5.4-12 12v40c0 6.6 5.4 12 12 12h40c6.6 0 12-5.4 12-12zm96 0v-40c0-6.6-5.4-12-12-12h-40c-6.6 0-12 5.4-12 12v40c0 6.6 5.4 12 12 12h40c6.6 0 12-5.4 12-12zm-96 96v-40c0-6.6-5.4-12-12-12h-40c-6.6 0-12 5.4-12 12v40c0 6.6 5.4 12 12 12h40c6.6 0 12-5.4 12-12zm-96 0v-40c0-6.6-5.4-12-12-12h-40c-6.6 0-12 5.4-12 12v40c0 6.6 5.4 12 12 12h40c6.6 0 12-5.4 12-12zm192 0v-40c0-6.6-5.4-12-12-12h-40c-6.6 0-12 5.4-12 12v40c0 6.6 5.4 12 12 12h40c6.6 0 12-5.4 12-12zm96-260v352c0 26.5-21.5 48-48 48H48c-26.5 0-48-21.5-48-48V112c0-26.5 21.5-48 48-48h48V12c0-6.6 5.4-12 12-12h40c6.6 0 12 5.4 12 12v52h128V12c0-6.6 5.4-12 12-12h40c6.6 0 12 5.4 12 12v52h48c26.5 0 48 21.5 48 48zm-48 346V160H48v298c0 3.3 2.7 6 6 6h340c3.3 0 6-2.7 6-6z\" fill=\"currentColor\"/>","width":448}},"width":512,"height":512});
|
||||
Iconify.addCollection({"prefix":"ph","icons":{"books-bold":{"body":"<path d=\"M237.479 194.54l-8.283-30.91l-33.128-123.64a20.02 20.02 0 0 0-24.495-14.14l-29.546 7.916A19.927 19.927 0 0 0 128 28H96a19.868 19.868 0 0 0-8 1.682A19.868 19.868 0 0 0 80 28H48a20.022 20.022 0 0 0-20 20v160a20.022 20.022 0 0 0 20 20h32a19.868 19.868 0 0 0 8-1.682A19.868 19.868 0 0 0 96 228h32a20.022 20.022 0 0 0 20-20v-69.214l19.932 74.389a19.99 19.99 0 0 0 24.495 14.141l30.91-8.282a20.022 20.022 0 0 0 14.142-24.494zM161.09 94.915l23.183-6.212l18.635 69.547l-23.182 6.212zm12.83-44.849l4.141 15.456l-23.182 6.211l-4.141-15.455zM124 164h-24V52h24zM76 52v16H52V52zM52 92h24v112H52zm48 112v-16h24v16zm90.08-.9l-4.142-15.456l23.182-6.212l4.141 15.456z\" fill=\"currentColor\"/>"}},"width":256,"height":256});
|
||||
Iconify.addCollection({"prefix":"ph","icons":{"books-bold":{"body":"<path d=\"M237.5 194.5l-8.3-30.9L196.1 40a20.1 20.1 0 0 0-24.5-14.2l-29.6 8a19.6 19.6 0 0 0-14-5.8H96a19.8 19.8 0 0 0-8 1.7a19.8 19.8 0 0 0-8-1.7H48a20.1 20.1 0 0 0-20 20v160a20.1 20.1 0 0 0 20 20h32a19.8 19.8 0 0 0 8-1.7a19.8 19.8 0 0 0 8 1.7h32a20.1 20.1 0 0 0 20-20v-69.2l19.9 74.4a20.1 20.1 0 0 0 19.4 14.8a17.9 17.9 0 0 0 5.1-.7l30.9-8.3a20 20 0 0 0 14.2-24.5zm-76.4-99.6l23.2-6.2l18.6 69.6l-23.2 6.2zm12.8-44.8l4.2 15.4l-23.2 6.2l-4.2-15.4zM124 164h-24V52h24zM76 52v16H52V52zM52 92h24v112H52zm48 112v-16h24v16zm90.1-.9l-4.2-15.5l23.2-6.2l4.2 15.5z\" fill=\"currentColor\"/>"}},"width":256,"height":256});
|
||||
Iconify.addCollection({"prefix":"fa-solid","icons":{"external-link-alt":{"body":"<path d=\"M432 320h-32a16 16 0 0 0-16 16v112H64V128h144a16 16 0 0 0 16-16V80a16 16 0 0 0-16-16H48a48 48 0 0 0-48 48v352a48 48 0 0 0 48 48h352a48 48 0 0 0 48-48V336a16 16 0 0 0-16-16zM488 0H360c-21.37 0-32.05 25.91-17 41l35.73 35.73L135 320.37a24 24 0 0 0 0 34L157.67 377a24 24 0 0 0 34 0l243.61-243.68L471 169c15 15 41 4.5 41-17V24a24 24 0 0 0-24-24z\" fill=\"currentColor\"/>"}},"width":512,"height":512});
|
||||
Iconify.addCollection({"prefix":"simple-icons","icons":{"wakatime":{"body":"<path d=\"M12 0C5.373 0 0 5.373 0 12s5.373 12 12 12s12-5.373 12-12S18.627 0 12 0zm0 2.824a9.176 9.176 0 1 1 0 18.352a9.176 9.176 0 0 1 0-18.352zm5.097 5.058c-.327 0-.61.19-.764.45c-1.025 1.463-2.21 3.162-3.288 4.706l-.387-.636a.897.897 0 0 0-.759-.439a.901.901 0 0 0-.788.492l-.357.581l-1.992-2.943a.897.897 0 0 0-.761-.446c-.514 0-.903.452-.903.96a1 1 0 0 0 .207.61l2.719 3.96c.152.272.44.47.776.47a.91.91 0 0 0 .787-.483c.046-.071.23-.368.314-.504l.324.52c-.035-.047.076.113.087.13c.024.031.054.059.078.085c.019.019.04.036.058.052c.036.033.08.056.115.08c.025.016.052.028.076.04c.029.015.06.024.088.035c.058.025.122.027.18.04c.031.004.064.003.092.005c.29 0 .546-.149.707-.36c1.4-2 2.842-4.055 4.099-5.849A.995.995 0 0 0 18 8.842c0-.508-.389-.96-.903-.96\" fill=\"currentColor\"/>"}},"width":24,"height":24});
|
||||
Iconify.addCollection({"prefix":"heroicons-solid","icons":{"light-bulb":{"body":"<g fill=\"none\"><path d=\"M11 3a1 1 0 1 0-2 0v1a1 1 0 1 0 2 0V3z\" fill=\"currentColor\"/><path d=\"M15.657 5.757a1 1 0 0 0-1.414-1.414l-.707.707a1 1 0 0 0 1.414 1.414l.707-.707z\" fill=\"currentColor\"/><path d=\"M18 10a1 1 0 0 1-1 1h-1a1 1 0 1 1 0-2h1a1 1 0 0 1 1 1z\" fill=\"currentColor\"/><path d=\"M5.05 6.464A1 1 0 1 0 6.464 5.05l-.707-.707a1 1 0 0 0-1.414 1.414l.707.707z\" fill=\"currentColor\"/><path d=\"M5 10a1 1 0 0 1-1 1H3a1 1 0 1 1 0-2h1a1 1 0 0 1 1 1z\" fill=\"currentColor\"/><path d=\"M8 16v-1h4v1a2 2 0 1 1-4 0z\" fill=\"currentColor\"/><path d=\"M12 14c.015-.34.208-.646.477-.859a4 4 0 1 0-4.954 0c.27.213.462.519.476.859h4.002z\" fill=\"currentColor\"/></g>"},"server":{"body":"<g fill=\"none\"><path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M2 5a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v2a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5zm14 1a1 1 0 1 1-2 0a1 1 0 0 1 2 0z\" fill=\"currentColor\"/><path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M2 13a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v2a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2v-2zm14 1a1 1 0 1 1-2 0a1 1 0 0 1 2 0z\" fill=\"currentColor\"/></g>"}},"width":20,"height":20});
|
||||
|
|
Binary file not shown.
|
@ -7,6 +7,11 @@ module.exports = {
|
|||
'newsbox-default',
|
||||
'newsbox-warning',
|
||||
'newsbox-danger',
|
||||
'leaderboard-self',
|
||||
'leaderboard-default',
|
||||
'leaderboard-gold',
|
||||
'leaderboard-silver',
|
||||
'leaderboard-bronze',
|
||||
]
|
||||
},
|
||||
}
|
|
@ -53,3 +53,10 @@ func ParseUserAgent(ua string) (string, string, error) {
|
|||
}
|
||||
return groups[0][1], groups[0][2], nil
|
||||
}
|
||||
|
||||
func SubSlice[T any](slice []T, from, to uint) []T {
|
||||
if int(to) > len(slice) {
|
||||
to = uint(len(slice))
|
||||
}
|
||||
return slice[from:int(to)]
|
||||
}
|
||||
|
|
|
@ -20,21 +20,44 @@
|
|||
|
||||
<main class="mt-10 flex-grow flex justify-center w-full" id="leaderboard-page">
|
||||
<div class="flex flex-col flex-grow mt-10">
|
||||
<h1 class="font-semibold text-3xl text-white m-0 mb-4">Leaderboard</h1>
|
||||
<h1 class="h1" style="margin-bottom: 0.5rem">Leaderboard</h1>
|
||||
|
||||
<ul class="flex space-x-4 mb-16 text-gray-600">
|
||||
<li class="font-semibold text-2xl {{ if eq .By "" }} text-gray-300 {{ else }} hover:text-gray-500 {{ end }}">
|
||||
<p class="block text-sm text-gray-300 w-full lg:w-3/4 mb-8">Wakapi's leaderboard shows a ranking of the most active users on this servers, given they opted in to get listed on the public leaderboard (see <i>Settings 🠒 Data</i>). Statistics are updated at least every 12 hours and are based on the users' total coding time in the past seven days. </p>
|
||||
|
||||
<ul class="flex space-x-4 mb-4 text-gray-600">
|
||||
<li class="font-semibold text-xl {{ if eq .By "" }} text-gray-300 {{ else }} hover:text-gray-500 {{ end }}">
|
||||
<a href="leaderboard">Total</a>
|
||||
</li>
|
||||
<li class="font-semibold text-2xl {{ if eq .By "language" }} text-gray-300 {{ else }} hover:text-gray-500 {{ end }}">
|
||||
<li class="font-semibold text-xl {{ if eq .By "language" }} text-gray-300 {{ else }} hover:text-gray-500 {{ end }}">
|
||||
<a href="leaderboard?by=language">By Language</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div id="total" class="tab flex flex-col space-y-4">
|
||||
{{ if ne .By "" }}
|
||||
<div class="flex space-x-2 mb-4">
|
||||
{{ range $i, $key := (strslice .TopKeys 0 20) }}
|
||||
<div class="inline-block">
|
||||
<a href="leaderboard?by={{ $.By }}&key={{ $key }}" class="{{ if eq $.Key (lower $key) }} btn-primary {{ else }} btn-default {{ end }} btn-small cursor-pointer">{{ $key }}</a>
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
<div class="flex flex-col space-y-4 mt-8 text-gray-300 w-full lg:w-3/4">
|
||||
<ol>
|
||||
{{ range $i, $item := .Items }}
|
||||
<li>{{ $item.Rank }} - {{ $item.UserID }} - {{ $item.Total | duration }}</li>
|
||||
<li class="px-4 py-2 my-2 rounded-md border-2 leaderboard-{{ ($.ColorModifier $item $.User) }} flex justify-between">
|
||||
<div class="w-2/12"><strong># {{ $item.Rank }}</strong></div>
|
||||
<div class="flex w-1/2 items-center space-x-4">
|
||||
{{ if avatarUrlTemplate }}
|
||||
<img src="{{ $item.User.AvatarURL avatarUrlTemplate }}" width="24px" class="rounded-full border-green-700" alt="User Profile Avatar"/>
|
||||
{{ else }}
|
||||
<span class="iconify inline cursor-pointer text-gray-500 rounded-full border-green-700" style="width: 24px; height: 24px" data-icon="ic:round-person"></span>
|
||||
{{ end }}
|
||||
<strong>@{{ $item.UserID }}</strong>
|
||||
</div>
|
||||
<div class="w-4/12 text-right"><span>{{ $item.Total | duration }}</span></div>
|
||||
</li>
|
||||
{{ end }}
|
||||
</ol>
|
||||
</div>
|
||||
|
|
Loading…
Reference in New Issue