From 7dc2551dee9179f8f9e460ee9dbe2f94b373b5d4 Mon Sep 17 00:00:00 2001 From: Thomas Miceli Date: Sat, 1 Feb 2025 12:14:40 +0100 Subject: [PATCH] Search gists on user profile --- go.mod | 1 + go.sum | 2 + internal/actions/actions.go | 17 ++++ internal/db/db.go | 2 +- internal/db/gist.go | 113 +++++++++++++++++++-- internal/db/gist_language.go | 27 +++++ internal/db/{gist_tag.go => gist_topic.go} | 0 internal/hooks/post_receive.go | 2 +- internal/i18n/locales/en-US.yml | 12 ++- internal/index/bleve.go | 2 +- internal/web/handlers/admin/actions.go | 6 ++ internal/web/handlers/admin/admin.go | 5 +- internal/web/handlers/gist/all.go | 66 ++++++++---- internal/web/handlers/gist/create.go | 1 + internal/web/handlers/gist/fork.go | 2 +- internal/web/handlers/gist/like.go | 2 +- internal/web/handlers/gist/revisions.go | 2 +- internal/web/handlers/util.go | 73 +++++++++++-- internal/web/server/router.go | 1 + public/main.ts | 34 +++++++ templates/pages/admin_index.html | 6 ++ templates/pages/all.html | 97 +++++++++++++++++- templates/partials/_pagination.html | 8 +- 23 files changed, 428 insertions(+), 53 deletions(-) create mode 100644 internal/db/gist_language.go rename internal/db/{gist_tag.go => gist_topic.go} (100%) diff --git a/go.mod b/go.mod index 9bc6302f..1bc28011 100644 --- a/go.mod +++ b/go.mod @@ -70,6 +70,7 @@ require ( github.com/golang/snappy v0.0.4 // indirect github.com/google/go-tpm v0.9.1 // indirect github.com/gorilla/mux v1.8.1 // indirect + github.com/gorilla/schema v1.4.1 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/pgx/v5 v5.7.1 // indirect diff --git a/go.sum b/go.sum index 57c72da4..142bbcb5 100644 --- a/go.sum +++ b/go.sum @@ -126,6 +126,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +github.com/gorilla/schema v1.4.1 h1:jUg5hUjCSDZpNGLuXQOgIWGdlgrIdYvgQ0wZtdK1M3E= +github.com/gorilla/schema v1.4.1/go.mod h1:Dg5SSm5PV60mhF2NFaTV1xuYYj8tV8NOPRo4FggUMnM= github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ= diff --git a/internal/actions/actions.go b/internal/actions/actions.go index 7a65ab6c..026fbfdf 100644 --- a/internal/actions/actions.go +++ b/internal/actions/actions.go @@ -23,6 +23,7 @@ const ( SyncGistPreviews ResetHooks IndexGists + SyncGistLanguages ) var ( @@ -73,6 +74,8 @@ func Run(actionType int) { functionToRun = resetHooks case IndexGists: functionToRun = indexGists + case SyncGistLanguages: + functionToRun = syncGistLanguages default: log.Error().Msg("Unknown action type") } @@ -166,3 +169,17 @@ func indexGists() { } } } + +func syncGistLanguages() { + log.Info().Msg("Syncing all Gist languages...") + gists, err := db.GetAllGistsRows() + if err != nil { + log.Error().Err(err).Msg("Cannot get gists") + return + } + + for _, gist := range gists { + log.Info().Msgf("Syncing languages for gist %d", gist.ID) + gist.UpdateLanguages() + } +} diff --git a/internal/db/db.go b/internal/db/db.go index 742592cd..3be2b1e3 100644 --- a/internal/db/db.go +++ b/internal/db/db.go @@ -144,7 +144,7 @@ func Setup(dbUri string) error { return err } - if err = db.AutoMigrate(&User{}, &Gist{}, &SSHKey{}, &AdminSetting{}, &Invitation{}, &WebAuthnCredential{}, &TOTP{}, &GistTopic{}); err != nil { + if err = db.AutoMigrate(&User{}, &Gist{}, &SSHKey{}, &AdminSetting{}, &Invitation{}, &WebAuthnCredential{}, &TOTP{}, &GistTopic{}, &GistLanguage{}); err != nil { return err } diff --git a/internal/db/gist.go b/internal/db/gist.go index 1f75fde6..600258d8 100644 --- a/internal/db/gist.go +++ b/internal/db/gist.go @@ -6,6 +6,7 @@ import ( "fmt" "os/exec" "path/filepath" + "slices" "strings" "time" @@ -50,16 +51,16 @@ func (v Visibility) Next() Visibility { } } -func ParseVisibility[T string | int](v T) (Visibility, error) { +func ParseVisibility[T string | int](v T) Visibility { switch s := fmt.Sprint(v); s { case "0", "public": - return PublicVisibility, nil + return PublicVisibility case "1", "unlisted": - return UnlistedVisibility, nil + return UnlistedVisibility case "2", "private": - return PrivateVisibility, nil + return PrivateVisibility default: - return -1, fmt.Errorf("unknown visibility %q", s) + return PublicVisibility } } @@ -84,7 +85,8 @@ type Gist struct { Forked *Gist `gorm:"foreignKey:ForkedID;constraint:OnUpdate:CASCADE,OnDelete:SET NULL"` ForkedID uint - Topics []GistTopic `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE"` + Topics []GistTopic `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE"` + Languages []GistLanguage `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE"` } type Like struct { @@ -166,25 +168,59 @@ func GetAllGistsFromSearch(currentUserId uint, query string, offset int, sort st } func gistsFromUserStatement(fromUserId uint, currentUserId uint) *gorm.DB { + return db. + Where("((gists.private = 0) or (gists.private > 0 and gists.user_id = ?))", currentUserId). + Where("users.id = ?", fromUserId). + Joins("join users on gists.user_id = users.id") +} + +func gistsFromUserStatementWithPreloads(fromUserId uint, currentUserId uint) *gorm.DB { return db.Preload("User").Preload("Forked.User").Preload("Topics"). Where("((gists.private = 0) or (gists.private > 0 and gists.user_id = ?))", currentUserId). Where("users.id = ?", fromUserId). Joins("join users on gists.user_id = users.id") } -func GetAllGistsFromUser(fromUserId uint, currentUserId uint, offset int, sort string, order string) ([]*Gist, error) { +func GetAllGistsFromUser(fromUserId uint, currentUserId uint, title string, language string, visibility string, topics []string, offset int, sort string, order string) ([]*Gist, int64, error) { var gists []*Gist - err := gistsFromUserStatement(fromUserId, currentUserId).Limit(11). + var count int64 + + baseQuery := gistsFromUserStatementWithPreloads(fromUserId, currentUserId).Model(&Gist{}) + + if title != "" { + baseQuery = baseQuery.Where("gists.title like ?", "%"+title+"%") + } + + if language != "" { + baseQuery = baseQuery.Joins("join gist_languages on gists.id = gist_languages.gist_id"). + Where("gist_languages.language = ?", language) + } + + if visibility != "" { + baseQuery = baseQuery.Where("gists.private = ?", ParseVisibility(visibility)) + } + + if len(topics) > 0 { + baseQuery = baseQuery.Joins("join gist_topics on gists.id = gist_topics.gist_id"). + Where("gist_topics.topic in ?", topics) + } + + err := baseQuery.Count(&count).Error + if err != nil { + return nil, 0, err + } + + err = baseQuery.Limit(11). Offset(offset * 10). Order("gists." + sort + "_at " + order). Find(&gists).Error - return gists, err + return gists, count, err } func CountAllGistsFromUser(fromUserId uint, currentUserId uint) (int64, error) { var count int64 - err := gistsFromUserStatement(fromUserId, currentUserId).Model(&Gist{}).Count(&count).Error + err := gistsFromUserStatementWithPreloads(fromUserId, currentUserId).Model(&Gist{}).Count(&count).Error return count, err } @@ -258,7 +294,18 @@ func GetAllGistsByIds(ids []uint) ([]*Gist, error) { Where("id in ?", ids). Find(&gists).Error - return gists, err + // keep order + ordered := make([]*Gist, 0, len(ids)) + for _, wantedId := range ids { + for _, gist := range gists { + if gist.ID == wantedId { + ordered = append(ordered, gist) + break + } + } + } + + return ordered, err } func (gist *Gist) Create() error { @@ -593,6 +640,47 @@ func DeserialiseInitRepository(user string) (*Gist, error) { return &gist, nil } +func (gist *Gist) UpdateLanguages() { + languages, err := gist.GetLanguagesFromFiles() + if err != nil { + log.Error().Err(err).Msgf("Cannot get languages for gist %d", gist.ID) + return + } + + slices.Sort(languages) + languages = slices.Compact(languages) + + tx := db.Begin() + if tx.Error != nil { + log.Error().Err(tx.Error).Msgf("Cannot start transaction for gist %d", gist.ID) + return + } + + if err := tx.Where("gist_id = ?", gist.ID).Delete(&GistLanguage{}).Error; err != nil { + tx.Rollback() + log.Error().Err(err).Msgf("Cannot delete languages for gist %d", gist.ID) + return + } + + for _, language := range languages { + gistLanguage := &GistLanguage{ + GistID: gist.ID, + Language: language, + } + if err := tx.Create(gistLanguage).Error; err != nil { + tx.Rollback() + log.Error().Err(err).Msgf("Cannot create gist language %s for gist %d", language, gist.ID) + return + } + } + + if err := tx.Commit().Error; err != nil { + tx.Rollback() + log.Error().Err(err).Msgf("Cannot commit transaction for gist %d", gist.ID) + return + } +} + func (gist *Gist) ToDTO() (*GistDTO, error) { files, err := gist.Files("HEAD", false) if err != nil { @@ -684,6 +772,9 @@ func (gist *Gist) ToIndexedGist() (*index.Gist, error) { wholeContent := "" for _, file := range files { wholeContent += file.Content + if !strings.HasSuffix(wholeContent, "\n") { + wholeContent += "\n" + } exts = append(exts, filepath.Ext(file.Filename)) } diff --git a/internal/db/gist_language.go b/internal/db/gist_language.go new file mode 100644 index 00000000..5b08d9bc --- /dev/null +++ b/internal/db/gist_language.go @@ -0,0 +1,27 @@ +package db + +type GistLanguage struct { + GistID uint `gorm:"primaryKey"` + Language string `gorm:"primaryKey;size:100"` +} + +func GetGistLanguagesForUser(fromUserId, currentUserId uint) ([]struct { + Language string + Count int64 +}, error) { + var results []struct { + Language string + Count int64 + } + + err := gistsFromUserStatement(fromUserId, currentUserId).Model(&GistLanguage{}). + Select("language, count(*) as count"). + Joins("JOIN gists ON gists.id = gist_languages.gist_id"). + Where("gists.user_id = ?", fromUserId). + Group("language"). + Order("count DESC"). + Limit(15). // Added limit of 15 + Find(&results).Error + + return results, err +} diff --git a/internal/db/gist_tag.go b/internal/db/gist_topic.go similarity index 100% rename from internal/db/gist_tag.go rename to internal/db/gist_topic.go diff --git a/internal/hooks/post_receive.go b/internal/hooks/post_receive.go index 628948c1..ccdf9abe 100644 --- a/internal/hooks/post_receive.go +++ b/internal/hooks/post_receive.go @@ -46,7 +46,7 @@ func PostReceive(in io.Reader, out, er io.Writer) error { } if slices.Contains([]string{"public", "unlisted", "private"}, opts["visibility"]) { - gist.Private, _ = db.ParseVisibility(opts["visibility"]) + gist.Private = db.ParseVisibility(opts["visibility"]) outputSb.WriteString(fmt.Sprintf("Gist visibility set to %s\n\n", opts["visibility"])) } diff --git a/internal/i18n/locales/en-US.yml b/internal/i18n/locales/en-US.yml index 3b15e3a6..cad30cfa 100644 --- a/internal/i18n/locales/en-US.yml +++ b/internal/i18n/locales/en-US.yml @@ -87,7 +87,15 @@ gist.search.help.filename: gists having files with given name gist.search.help.extension: gists having files with given extension gist.search.help.language: gists having files with given language gist.search.help.topic: gists with given topic - +gist.search.placeholder.title: Title +gist.search.placeholder.visibility: Visibility +gist.search.placeholder.public: Public +gist.search.placeholder.unlisted: Unlisted +gist.search.placeholder.private: Private +gist.search.placeholder.language: Language +gist.search.placeholder.all: All +gist.search.placeholder.topics: Topics +gist.search.placeholder.search: Search gist.forks: Forks gist.forks.view: View fork @@ -234,6 +242,7 @@ admin.actions.git-gc: Garbage collect all git repositories admin.actions.sync-previews: Synchronize all gists previews admin.actions.reset-hooks: Reset Git server hooks for all repositories admin.actions.index-gists: Index all gists +admin.actions.sync-gist-languages: Synchronize all gists languages admin.id: ID admin.user: User admin.delete: Delete @@ -279,6 +288,7 @@ flash.admin.git-gc: Garbage collecting repositories... flash.admin.sync-previews: Syncing Gist previews... flash.admin.reset-hooks: Resetting Git server hooks for all repositories... flash.admin.index-gists: Indexing all gists... +flash.admin.sync-gist-languages: Syncing Gist languages... flash.auth.username-exists: Username already exists flash.auth.invalid-credentials: Invalid credentials diff --git a/internal/index/bleve.go b/internal/index/bleve.go index f823c16d..7212a659 100644 --- a/internal/index/bleve.go +++ b/internal/index/bleve.go @@ -177,7 +177,7 @@ func SearchGists(queryStr string, queryMetadata SearchGistMetadata, gistsIds []u perPage := 10 offset := (page - 1) * perPage - s := bleve.NewSearchRequestOptions(indexerQuery, perPage, offset, false) + s := bleve.NewSearchRequestOptions(indexerQuery, perPage+1, offset, false) s.AddFacet("languageFacet", languageFacet) s.Fields = []string{"GistID"} s.IncludeLocations = false diff --git a/internal/web/handlers/admin/actions.go b/internal/web/handlers/admin/actions.go index 3683930d..06e47448 100644 --- a/internal/web/handlers/admin/actions.go +++ b/internal/web/handlers/admin/actions.go @@ -40,3 +40,9 @@ func AdminIndexGists(ctx *context.Context) error { go actions.Run(actions.IndexGists) return ctx.RedirectTo("/admin-panel") } + +func AdminSyncGistLanguages(ctx *context.Context) error { + ctx.AddFlash(ctx.Tr("flash.admin.sync-gist-languages"), "success") + go actions.Run(actions.SyncGistLanguages) + return ctx.RedirectTo("/admin-panel") +} diff --git a/internal/web/handlers/admin/admin.go b/internal/web/handlers/admin/admin.go index 13a47444..9ff5085a 100644 --- a/internal/web/handlers/admin/admin.go +++ b/internal/web/handlers/admin/admin.go @@ -48,6 +48,7 @@ func AdminIndex(ctx *context.Context) error { ctx.SetData("syncGistPreviews", actions.IsRunning(actions.SyncGistPreviews)) ctx.SetData("resetHooks", actions.IsRunning(actions.ResetHooks)) ctx.SetData("indexGists", actions.IsRunning(actions.IndexGists)) + ctx.SetData("syncGistLanguages", actions.IsRunning(actions.SyncGistLanguages)) return ctx.Html("admin_index.html") } @@ -64,7 +65,7 @@ func AdminUsers(ctx *context.Context) error { return ctx.ErrorRes(500, "Cannot get users", err) } - if err = handlers.Paginate(ctx, data, pageInt, 10, "data", "admin-panel/users", 1); err != nil { + if err = handlers.Paginate(ctx, data, pageInt, 10, "data", "admin-panel/users", 1, nil); err != nil { return ctx.ErrorRes(404, ctx.Tr("error.page-not-found"), nil) } @@ -82,7 +83,7 @@ func AdminGists(ctx *context.Context) error { return ctx.ErrorRes(500, "Cannot get gists", err) } - if err = handlers.Paginate(ctx, data, pageInt, 10, "data", "admin-panel/gists", 1); err != nil { + if err = handlers.Paginate(ctx, data, pageInt, 10, "data", "admin-panel/gists", 1, nil); err != nil { return ctx.ErrorRes(404, ctx.Tr("error.page-not-found"), nil) } diff --git a/internal/web/handlers/gist/all.go b/internal/web/handlers/gist/all.go index 489660b4..4467bc0d 100644 --- a/internal/web/handlers/gist/all.go +++ b/internal/web/handlers/gist/all.go @@ -9,7 +9,8 @@ import ( "github.com/thomiceli/opengist/internal/web/context" "github.com/thomiceli/opengist/internal/web/handlers" "gorm.io/gorm" - "html/template" + "slices" + "strings" ) func AllGists(ctx *context.Context) error { @@ -35,6 +36,11 @@ func AllGists(ctx *context.Context) error { orderText = ctx.TrH("gist.list.order-by-asc") } + pagination := &handlers.PaginationParams{ + Sort: sort, + Order: order, + } + ctx.SetData("sort", sortText) ctx.SetData("order", orderText) @@ -51,7 +57,7 @@ func AllGists(ctx *context.Context) error { if mode == "search" { ctx.SetData("htmlTitle", ctx.TrH("gist.list.search-results")) ctx.SetData("searchQuery", ctx.QueryParam("q")) - ctx.SetData("searchQueryUrl", template.URL("&q="+ctx.QueryParam("q"))) + pagination.Query = ctx.QueryParam("q") urlPage = "search" gists, err = db.GetAllGistsFromSearch(currentUserId, ctx.QueryParam("q"), pageInt-1, sort, order, "") } else if mode == "topics" { @@ -66,6 +72,7 @@ func AllGists(ctx *context.Context) error { } } else { var fromUser *db.User + var count int64 fromUser, err = db.GetUserByUsername(fromUserStr) if err != nil { @@ -104,10 +111,39 @@ func AllGists(ctx *context.Context) error { gists, err = db.GetAllGistsForkedByUser(fromUser.ID, currentUserId, pageInt-1, sort, order) } else if mode == "fromUser" { urlPage = fromUserStr + languages, err := db.GetGistLanguagesForUser(fromUser.ID, currentUserId) + if err != nil { + return ctx.ErrorRes(500, "Error fetching languages", err) + } + ctx.SetData("languages", languages) + + title := ctx.QueryParam("title") + language := ctx.QueryParam("language") + visibility := ctx.QueryParam("visibility") + topicsStr := ctx.QueryParam("topics") + topics := strings.Fields(topicsStr) + if len(topics) > 10 { + topics = topics[:10] + } + slices.Sort(topics) + topics = slices.Compact(topics) + pagination.Title = title + pagination.Language = language + pagination.Visibility = visibility + pagination.Topics = topicsStr + + ctx.SetData("title", title) + ctx.SetData("language", language) + ctx.SetData("visibility", visibility) + ctx.SetData("topics", topicsStr) ctx.SetData("htmlTitle", ctx.TrH("gist.list.all-from", fromUserStr)) - gists, err = db.GetAllGistsFromUser(fromUser.ID, currentUserId, pageInt-1, sort, order) + gists, count, err = db.GetAllGistsFromUser(fromUser.ID, currentUserId, title, language, visibility, topics, pageInt-1, sort, order) + ctx.SetData("countFromUser", count) } } + if err != nil { + return ctx.ErrorRes(500, "Error fetching gists", err) + } renderedGists := make([]*render.RenderedGist, 0, len(gists)) for _, gist := range gists { @@ -118,21 +154,20 @@ func AllGists(ctx *context.Context) error { renderedGists = append(renderedGists, &rendered) } - if err != nil { - return ctx.ErrorRes(500, "Error fetching gists", err) - } - - if err = handlers.Paginate(ctx, renderedGists, pageInt, 10, "gists", fromUserStr, 2, "&sort="+sort+"&order="+order); err != nil { + if err = handlers.Paginate(ctx, renderedGists, pageInt, 10, "gists", urlPage, 2, pagination); err != nil { return ctx.ErrorRes(404, ctx.Tr("error.page-not-found"), nil) } - ctx.SetData("urlPage", urlPage) return ctx.Html("all.html") } func Search(ctx *context.Context) error { var err error + pagination := &handlers.PaginationParams{ + Query: ctx.QueryParam("q"), + } + content, meta := handlers.ParseSearchQueryStr(ctx.QueryParam("q")) pageInt := handlers.GetPage(ctx) @@ -176,19 +211,12 @@ func Search(ctx *context.Context) error { renderedGists = append(renderedGists, &rendered) } - if pageInt > 1 && len(renderedGists) != 0 { - ctx.SetData("prevPage", pageInt-1) - } - if 10*pageInt < int(nbHits) { - ctx.SetData("nextPage", pageInt+1) + if err = handlers.Paginate(ctx, renderedGists, pageInt, 10, "gists", "search", 2, pagination); err != nil { + return ctx.ErrorRes(404, ctx.Tr("error.page-not-found"), nil) } - ctx.SetData("prevLabel", ctx.TrH("pagination.previous")) - ctx.SetData("nextLabel", ctx.TrH("pagination.next")) - ctx.SetData("urlPage", "search") - ctx.SetData("urlParams", template.URL("&q="+ctx.QueryParam("q"))) + ctx.SetData("htmlTitle", ctx.TrH("gist.list.search-results")) ctx.SetData("nbHits", nbHits) - ctx.SetData("gists", renderedGists) ctx.SetData("langs", langs) ctx.SetData("searchQuery", ctx.QueryParam("q")) return ctx.Html("search.html") diff --git a/internal/web/handlers/gist/create.go b/internal/web/handlers/gist/create.go index b71fe079..b017adc0 100644 --- a/internal/web/handlers/gist/create.go +++ b/internal/web/handlers/gist/create.go @@ -137,6 +137,7 @@ func ProcessCreate(ctx *context.Context) error { } gist.AddInIndex() + gist.UpdateLanguages() return ctx.RedirectTo("/" + user.Username + "/" + gist.Identifier()) } diff --git a/internal/web/handlers/gist/fork.go b/internal/web/handlers/gist/fork.go index 96f2fb30..84985079 100644 --- a/internal/web/handlers/gist/fork.go +++ b/internal/web/handlers/gist/fork.go @@ -77,7 +77,7 @@ func Forks(ctx *context.Context) error { return ctx.ErrorRes(500, "Error getting users who liked this gist", err) } - if err = handlers.Paginate(ctx, forks, pageInt, 30, "forks", gist.User.Username+"/"+gist.Identifier()+"/forks", 2); err != nil { + if err = handlers.Paginate(ctx, forks, pageInt, 30, "forks", gist.User.Username+"/"+gist.Identifier()+"/forks", 2, nil); err != nil { return ctx.ErrorRes(404, ctx.Tr("error.page-not-found"), nil) } diff --git a/internal/web/handlers/gist/like.go b/internal/web/handlers/gist/like.go index 177d358a..f6fc424d 100644 --- a/internal/web/handlers/gist/like.go +++ b/internal/web/handlers/gist/like.go @@ -42,7 +42,7 @@ func Likes(ctx *context.Context) error { return ctx.ErrorRes(500, "Error getting users who liked this gist", err) } - if err = handlers.Paginate(ctx, likers, pageInt, 30, "likers", gist.User.Username+"/"+gist.Identifier()+"/likes", 1); err != nil { + if err = handlers.Paginate(ctx, likers, pageInt, 30, "likers", gist.User.Username+"/"+gist.Identifier()+"/likes", 1, nil); err != nil { return ctx.ErrorRes(404, ctx.Tr("error.page-not-found"), nil) } diff --git a/internal/web/handlers/gist/revisions.go b/internal/web/handlers/gist/revisions.go index 7f94076a..b98bdc07 100644 --- a/internal/web/handlers/gist/revisions.go +++ b/internal/web/handlers/gist/revisions.go @@ -19,7 +19,7 @@ func Revisions(ctx *context.Context) error { return ctx.ErrorRes(500, "Error fetching commits log", err) } - if err := handlers.Paginate(ctx, commits, pageInt, 10, "commits", userName+"/"+gistName+"/revisions", 2); err != nil { + if err := handlers.Paginate(ctx, commits, pageInt, 10, "commits", userName+"/"+gistName+"/revisions", 2, nil); err != nil { return ctx.ErrorRes(404, ctx.Tr("error.page-not-found"), nil) } diff --git a/internal/web/handlers/util.go b/internal/web/handlers/util.go index da29bb6e..af39240a 100644 --- a/internal/web/handlers/util.go +++ b/internal/web/handlers/util.go @@ -2,7 +2,9 @@ package handlers import ( "errors" + "github.com/gorilla/schema" "html/template" + "net/url" "path/filepath" "strconv" "strings" @@ -24,7 +26,68 @@ func GetPage(ctx *context.Context) int { return pageInt } -func Paginate[T any](ctx *context.Context, data []*T, pageInt int, perPage int, templateDataName string, urlPage string, labels int, urlParams ...string) error { +type PaginationParams struct { + Page int `schema:"page,omitempty"` + Sort string `schema:"sort,omitempty"` + Order string `schema:"order,omitempty"` + Title string `schema:"title,omitempty"` + Visibility string `schema:"visibility,omitempty"` + Language string `schema:"language,omitempty"` + Topics string `schema:"topics,omitempty"` + Query string `schema:"q,omitempty"` + + HasPrevious bool `schema:"-"` // Exclude from URL parameters + HasNext bool `schema:"-"` +} + +var encoder = schema.NewEncoder() + +func (p PaginationParams) String() string { + values := url.Values{} + + err := encoder.Encode(p, values) + if err != nil { + return "" + } + + if len(values) == 0 { + return "" + } + return "?" + values.Encode() +} + +func (p PaginationParams) NextURL() template.URL { + p.Page++ + return template.URL(p.String()) +} + +func (p PaginationParams) PreviousURL() template.URL { + p.Page-- + return template.URL(p.String()) +} + +func (p PaginationParams) WithParams(pairs ...string) template.URL { + values := url.Values{} + _ = encoder.Encode(p, values) + + // reset page + values.Del("page") + + for i := 0; i < len(pairs); i += 2 { + values.Set(pairs[i], pairs[i+1]) + } + + return template.URL("?" + values.Encode()) +} + +func Paginate[T any](ctx *context.Context, data []*T, pageInt int, perPage int, templateDataName string, urlPage string, labels int, params *PaginationParams) error { + var paginationParams PaginationParams + if params == nil { + paginationParams = PaginationParams{} + } else { + paginationParams = *params + } + paginationParams.Page = pageInt lenData := len(data) if lenData == 0 && pageInt != 1 { return errors.New("page not found") @@ -34,15 +97,13 @@ func Paginate[T any](ctx *context.Context, data []*T, pageInt int, perPage int, if lenData > 1 { data = data[:lenData-1] } - ctx.SetData("nextPage", pageInt+1) + paginationParams.HasNext = true } if pageInt > 1 { - ctx.SetData("prevPage", pageInt-1) + paginationParams.HasPrevious = true } - if len(urlParams) > 0 { - ctx.SetData("urlParams", template.URL(urlParams[0])) - } + ctx.SetData("pagination", paginationParams) switch labels { case 1: diff --git a/internal/web/server/router.go b/internal/web/server/router.go index 4ddeaab1..6948606f 100644 --- a/internal/web/server/router.go +++ b/internal/web/server/router.go @@ -82,6 +82,7 @@ func (s *Server) registerRoutes() { sB.POST("/sync-previews", admin.AdminSyncGistPreviews) sB.POST("/reset-hooks", admin.AdminResetHooks) sB.POST("/index-gists", admin.AdminIndexGists) + sB.POST("/sync-languages", admin.AdminSyncGistLanguages) sB.GET("/configuration", admin.AdminConfig) sB.PUT("/set-config", admin.AdminSetConfig) } diff --git a/public/main.ts b/public/main.ts index 0d54a2cc..7fc2167e 100644 --- a/public/main.ts +++ b/public/main.ts @@ -161,6 +161,40 @@ document.addEventListener('DOMContentLoaded', () => { }; } + const searchUserGistsVisibility = document.getElementById('search-user-gists-visibility'); + if (searchUserGistsVisibility) { + let dropdown = document.getElementById('search-user-gists-visibility-dropdown'); + searchUserGistsVisibility.onclick = () => { + dropdown!.classList.toggle('hidden'); + }; + + let buttons = dropdown.querySelectorAll('button'); + buttons.forEach((button) => { + button.onclick = () => { + let value = document.getElementById('visibility-value') as HTMLInputElement; + value.textContent = button.dataset.visibilityStr; + dropdown!.classList.add('hidden'); + dropdown.querySelector('input')!.value = button.dataset.visibility || ''; + }; + }); + } + + const searchUserGistsLanguage = document.getElementById('search-user-gists-language'); + if (searchUserGistsLanguage) { + let dropdown = document.getElementById('search-user-gists-language-dropdown'); + searchUserGistsLanguage.onclick = () => { + dropdown!.classList.toggle('hidden'); + }; + let buttons = dropdown.querySelectorAll('button'); + buttons.forEach((button) => { + button.onclick = () => { + let value = document.getElementById('language-value') as HTMLInputElement; + value.textContent = button.dataset.languageStr; + dropdown!.classList.add('hidden'); + dropdown.querySelector('input')!.value = button.dataset.language || ''; + }; + }); + } document.getElementById('language-btn')!.onclick = () => { document.getElementById('language-list')!.classList.toggle('hidden'); }; diff --git a/templates/pages/admin_index.html b/templates/pages/admin_index.html index 83778d65..0c391232 100644 --- a/templates/pages/admin_index.html +++ b/templates/pages/admin_index.html @@ -92,6 +92,12 @@ {{ .locale.Tr "admin.actions.index-gists" }} +
+ {{ .csrfHtml }} + +
diff --git a/templates/pages/all.html b/templates/pages/all.html index 7ad4bb78..b9c7be5d 100644 --- a/templates/pages/all.html +++ b/templates/pages/all.html @@ -43,22 +43,22 @@

{{ .locale.Tr "gist.list.topic-resu