mirror of
https://github.com/thomiceli/opengist.git
synced 2025-02-06 10:24:05 +00:00
Search gists on user profile with title, visibility, language & topics (#422)
This commit is contained in:
parent
76fc129c09
commit
7aa8f84eff
1
go.mod
1
go.mod
@ -11,6 +11,7 @@ require (
|
||||
github.com/go-playground/validator/v10 v10.23.0
|
||||
github.com/go-webauthn/webauthn v0.11.2
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gorilla/schema v1.4.1
|
||||
github.com/gorilla/securecookie v1.1.2
|
||||
github.com/gorilla/sessions v1.4.0
|
||||
github.com/labstack/echo/v4 v4.12.0
|
||||
|
2
go.sum
2
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=
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
@ -258,5 +258,5 @@ func DeprecationDBFilename() {
|
||||
}
|
||||
|
||||
func TruncateDatabase() error {
|
||||
return db.Migrator().DropTable("likes", &User{}, "gists", &SSHKey{}, &AdminSetting{}, &Invitation{}, &WebAuthnCredential{}, &TOTP{}, &GistTopic{})
|
||||
return db.Migrator().DropTable("likes", &User{}, "gists", &SSHKey{}, &AdminSetting{}, &Invitation{}, &WebAuthnCredential{}, &TOTP{}, &GistTopic{}, &GistLanguage{})
|
||||
}
|
||||
|
@ -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))
|
||||
}
|
||||
|
||||
|
27
internal/db/gist_language.go
Normal file
27
internal/db/gist_language.go
Normal file
@ -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
|
||||
}
|
@ -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"]))
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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")
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
||||
if languages, err := db.GetGistLanguagesForUser(fromUser.ID, currentUserId); err != nil {
|
||||
return ctx.ErrorRes(500, "Error fetching languages", err)
|
||||
} else {
|
||||
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 err = handlers.Paginate(ctx, renderedGists, pageInt, 10, "gists", "search", 2, pagination); err != nil {
|
||||
return ctx.ErrorRes(404, ctx.Tr("error.page-not-found"), nil)
|
||||
}
|
||||
if 10*pageInt < int(nbHits) {
|
||||
ctx.SetData("nextPage", pageInt+1)
|
||||
}
|
||||
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")
|
||||
|
@ -137,6 +137,7 @@ func ProcessCreate(ctx *context.Context) error {
|
||||
}
|
||||
|
||||
gist.AddInIndex()
|
||||
gist.UpdateLanguages()
|
||||
|
||||
return ctx.RedirectTo("/" + user.Username + "/" + gist.Identifier())
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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');
|
||||
};
|
||||
|
6
templates/pages/admin_index.html
vendored
6
templates/pages/admin_index.html
vendored
@ -92,6 +92,12 @@
|
||||
{{ .locale.Tr "admin.actions.index-gists" }}
|
||||
</button>
|
||||
</form>
|
||||
<form action="{{ $.c.ExternalUrl }}/admin-panel/sync-languages" method="POST">
|
||||
{{ .csrfHtml }}
|
||||
<button type="submit" {{ if .syncGistLanguages }}disabled="disabled"{{ end }} class="whitespace-nowrap text-slate-700 dark:text-slate-300{{ if .syncGistLanguages }} text-slate-500 cursor-not-allowed {{ end }}rounded border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 px-2.5 py-2 text-xs font-medium text-gray-700 dark:text-white shadow-sm hover:bg-gray-100 dark:hover:bg-gray-700 hover:border-gray-500 hover:text-slate-700 dark:hover:text-slate-300 focus:outline-none focus:ring-1 focus:border-primary-500 focus:ring-primary-500 leading-3">
|
||||
{{ .locale.Tr "admin.actions.sync-gist-languages" }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
97
templates/pages/all.html
vendored
97
templates/pages/all.html
vendored
@ -43,22 +43,22 @@
|
||||
</div>
|
||||
<div id="sort-gists-dropdown" class="hidden absolute right-0 z-10 mt-2 w-max origin-top-right divide-y divide-gray-200 dark:divide-gray-700 rounded-md rounded border border-gray-200 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 shadow-lg ring-1 ring-white dark:ring-black ring-opacity-5 focus:outline-none" role="menu" aria-orientation="vertical" aria-labelledby="menu-button" tabindex="-1">
|
||||
<div class="" role="none">
|
||||
<a href="{{ $.c.ExternalUrl }}/{{ .urlPage }}?sort=created&order=desc{{.searchQueryUrl}}" class="text-slate-700 dark:text-slate-300 group flex items-center px-3 py-2 text-xs hover:bg-gray-200 dark:hover:bg-gray-700 hover:text-black dark:hover:text-white hover:text-white hover:bg-primary-500 hover:rounded-t-md" role="menuitem">
|
||||
<a href="{{ $.c.ExternalUrl }}/{{ .urlPage }}{{ .pagination.WithParams "sort" "created" "order" "desc" }}" class="text-slate-700 dark:text-slate-300 group flex items-center px-3 py-2 text-xs hover:bg-gray-200 dark:hover:bg-gray-700 hover:text-black dark:hover:text-white hover:text-white hover:bg-primary-500 hover:rounded-t-md" role="menuitem">
|
||||
{{ .locale.Tr "gist.list.order-by-desc" }} {{ .locale.Tr "gist.list.sort-by-created" }}
|
||||
</a>
|
||||
</div>
|
||||
<div class="" role="none">
|
||||
<a href="{{ $.c.ExternalUrl }}/{{ .urlPage }}?sort=created&order=asc{{.searchQueryUrl}}" class="text-slate-700 dark:text-slate-300 group flex items-center px-3 py-2 text-xs hover:bg-gray-200 dark:hover:bg-gray-700 hover:text-black dark:hover:text-white hover:text-white hover:bg-primary-500" role="menuitem">
|
||||
<a href="{{ $.c.ExternalUrl }}/{{ .urlPage }}{{ .pagination.WithParams "sort" "created" "order" "asc" }}" class="text-slate-700 dark:text-slate-300 group flex items-center px-3 py-2 text-xs hover:bg-gray-200 dark:hover:bg-gray-700 hover:text-black dark:hover:text-white hover:text-white hover:bg-primary-500" role="menuitem">
|
||||
{{ .locale.Tr "gist.list.order-by-asc" }} {{ .locale.Tr "gist.list.sort-by-created" }}
|
||||
</a>
|
||||
</div>
|
||||
<div class="" role="none">
|
||||
<a href="{{ $.c.ExternalUrl }}/{{ .urlPage }}?sort=updated&order=desc{{.searchQueryUrl}}" class="text-slate-700 dark:text-slate-300 group flex items-center px-3 py-2 text-xs hover:bg-gray-200 dark:hover:bg-gray-700 hover:text-black dark:hover:text-white hover:text-white hover:bg-primary-500" role="menuitem">
|
||||
<a href="{{ $.c.ExternalUrl }}/{{ .urlPage }}{{ .pagination.WithParams "sort" "updated" "order" "desc" }}" class="text-slate-700 dark:text-slate-300 group flex items-center px-3 py-2 text-xs hover:bg-gray-200 dark:hover:bg-gray-700 hover:text-black dark:hover:text-white hover:text-white hover:bg-primary-500" role="menuitem">
|
||||
{{ .locale.Tr "gist.list.order-by-desc" }} {{ .locale.Tr "gist.list.sort-by-updated" }}
|
||||
</a>
|
||||
</div>
|
||||
<div class="" role="none">
|
||||
<a href="{{ $.c.ExternalUrl }}/{{ .urlPage }}?sort=updated&order=asc{{.searchQueryUrl}}" class="text-slate-700 dark:text-slate-300 group flex items-center px-3 py-2 text-xs hover:bg-gray-200 dark:hover:bg-gray-700 hover:text-black dark:hover:text-white hover:text-white hover:bg-primary-500 hover:rounded-b-md" role="menuitem">
|
||||
<a href="{{ $.c.ExternalUrl }}/{{ .urlPage }}{{ .pagination.WithParams "sort" "updated" "order" "asc" }}" class="text-slate-700 dark:text-slate-300 group flex items-center px-3 py-2 text-xs hover:bg-gray-200 dark:hover:bg-gray-700 hover:text-black dark:hover:text-white hover:text-white hover:bg-primary-500 hover:rounded-b-md" role="menuitem">
|
||||
{{ .locale.Tr "gist.list.order-by-asc" }} {{ .locale.Tr "gist.list.sort-by-updated" }}
|
||||
</a>
|
||||
</div>
|
||||
@ -115,6 +115,95 @@
|
||||
{{ end }}
|
||||
</header>
|
||||
<main>
|
||||
{{if eq .mode "fromUser"}}
|
||||
<form action="{{ $.c.ExternalUrl }}/{{ .fromUser.Username }}">
|
||||
<div class="grid grid-cols-12 gap-x-1 pb-4">
|
||||
<div class="col-span-3">
|
||||
<input type="text" name="title" value="{{ .title }}" placeholder="{{ .locale.Tr "gist.search.placeholder.title"}}" class="bg-white dark:bg-gray-900 shadow-sm focus:ring-primary-500 focus:border-primary-500 block w-full sm:text-xs border-gray-200 dark:border-gray-700 rounded-md py-1.5" />
|
||||
</div>
|
||||
<div class="col-span-2">
|
||||
<div class="">
|
||||
<div class="relative text-left">
|
||||
<div>
|
||||
<button type="button" class="w-full flex text-slate-700 dark:text-slate-300 rounded border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 px-2.5 py-2 text-xs font-medium text-gray-700 dark:text-white shadow-sm hover:bg-gray-100 dark:hover:bg-gray-700 hover:border-gray-500 hover:text-slate-700 dark:hover:text-slate-300 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-primary-500 leading-3" id="search-user-gists-visibility">
|
||||
<span class="text-gray-700 dark:text-gray-300">{{ .locale.Tr "gist.search.placeholder.visibility" }} :
|
||||
<span id="visibility-value" class="text-slate-700 dark:text-slate-300">
|
||||
{{ if eq .visibility "public" }}{{ .locale.Tr "gist.search.placeholder.public" }}
|
||||
{{ else if eq .visibility "unlisted" }}{{ .locale.Tr "gist.search.placeholder.unlisted" }}
|
||||
{{ else if eq .visibility "private" }}{{ .locale.Tr "gist.search.placeholder.private" }}
|
||||
{{ else }}{{ .locale.Tr "gist.search.placeholder.all" }}
|
||||
{{ end }}
|
||||
</span>
|
||||
</span>
|
||||
<svg class="-mr-1 ml-2 h-3 w-3" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||
<path fill-rule="evenodd" d="M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div id="search-user-gists-visibility-dropdown" class="hidden absolute left-0 z-10 mt-2 w-max origin-top-right divide-y divide-gray-200 dark:divide-gray-700 rounded-md rounded border border-gray-200 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 shadow-lg ring-1 ring-white dark:ring-black ring-opacity-5 focus:outline-none" role="menu" aria-orientation="vertical" aria-labelledby="menu-button" tabindex="-1">
|
||||
<div class="" role="none">
|
||||
<button type="button" data-visibility="" data-visibility-str="{{ .locale.Tr "gist.search.placeholder.all" }}" class="text-slate-700 dark:text-slate-300 w-full flex px-3 py-2 text-xs hover:bg-gray-200 dark:hover:bg-gray-700 hover:text-black dark:hover:text-white hover:text-white hover:bg-primary-500 hover:rounded-t-md" role="menuitem">
|
||||
{{ .locale.Tr "gist.search.placeholder.all" }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="" role="none">
|
||||
<button type="button" data-visibility="public" data-visibility-str="{{ .locale.Tr "gist.search.placeholder.public" }}" class="text-slate-700 dark:text-slate-300 w-full flex px-3 py-2 text-xs hover:bg-gray-200 dark:hover:bg-gray-700 hover:text-black dark:hover:text-white hover:text-white hover:bg-primary-500" role="menuitem">
|
||||
{{ .locale.Tr "gist.search.placeholder.public" }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="" role="none">
|
||||
<button type="button" data-visibility="unlisted" data-visibility-str="{{ .locale.Tr "gist.search.placeholder.unlisted" }}" class="text-slate-700 dark:text-slate-300 w-full flex px-3 py-2 text-xs hover:bg-gray-200 dark:hover:bg-gray-700 hover:text-black dark:hover:text-white hover:text-white hover:bg-primary-500" role="menuitem">
|
||||
{{ .locale.Tr "gist.search.placeholder.unlisted" }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="" role="none">
|
||||
<button type="button" data-visibility="private" data-visibility-str="{{ .locale.Tr "gist.search.placeholder.private" }}" class="text-slate-700 dark:text-slate-300 w-full flex px-3 py-2 text-xs hover:bg-gray-200 dark:hover:bg-gray-700 hover:text-black dark:hover:text-white hover:text-white hover:bg-primary-500 hover:rounded-b-md" role="menuitem">
|
||||
{{ .locale.Tr "gist.search.placeholder.private" }}
|
||||
</button>
|
||||
</div>
|
||||
<input type="hidden" name="visibility" value="{{ .visibility }}" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-span-3">
|
||||
<div class="align-middle items-center">
|
||||
<div class="relative text-left">
|
||||
<div>
|
||||
<button type="button" class="w-full flex text-slate-700 dark:text-slate-300 rounded border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 px-2.5 py-2 text-xs font-medium text-gray-700 dark:text-white shadow-sm hover:bg-gray-100 dark:hover:bg-gray-700 hover:border-gray-500 hover:text-slate-700 dark:hover:text-slate-300 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-primary-500 leading-3" id="search-user-gists-language">
|
||||
<span class="text-gray-700 dark:text-gray-300">{{ .locale.Tr "gist.search.placeholder.language" }} :
|
||||
<span id="language-value" class="text-slate-700 dark:text-slate-300">
|
||||
{{ if eq .language "" }}{{ .locale.Tr "gist.search.placeholder.all" }}
|
||||
{{ else }}{{ .language }}
|
||||
{{ end }}</span></span>
|
||||
<svg class="-mr-1 ml-2 h-3 w-3" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||
<path fill-rule="evenodd" d="M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div id="search-user-gists-language-dropdown" class="hidden absolute left-0 z-10 mt-2 w-max origin-top-right divide-y divide-gray-200 dark:divide-gray-700 rounded-md rounded border border-gray-200 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 shadow-lg ring-1 ring-white dark:ring-black ring-opacity-5 focus:outline-none" role="menu" aria-orientation="vertical" aria-labelledby="menu-button" tabindex="-1">
|
||||
<button type="button" data-language="" data-language-str="{{ .locale.Tr "gist.search.placeholder.all" }}" class="text-slate-700 dark:text-slate-300 w-full flex px-3 py-2 text-xs hover:bg-gray-200 dark:hover:bg-gray-700 hover:text-black dark:hover:text-white hover:text-white hover:bg-primary-500 first:hover:rounded-t-md last:hover:rounded-b-md" role="menuitem">
|
||||
{{ .locale.Tr "gist.search.placeholder.all" }}
|
||||
</button>
|
||||
{{ range .languages }}
|
||||
<button type="button" data-language="{{ .Language }}" data-language-str="{{ .Language }}" class="text-slate-700 dark:text-slate-300 w-full flex px-3 py-2 text-xs hover:bg-gray-200 dark:hover:bg-gray-700 hover:text-black dark:hover:text-white hover:text-white hover:bg-primary-500 first:hover:rounded-t-md last:hover:rounded-b-md" role="menuitem">
|
||||
{{ .Language }} ({{ .Count }})
|
||||
</button>
|
||||
{{ end }}
|
||||
<input type="hidden" name="language" value="{{ .language }}" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-span-2">
|
||||
<input type="text" name="topics" value="{{ .topics }}" placeholder="{{ .locale.Tr "gist.search.placeholder.topics"}}" class="bg-white dark:bg-gray-900 shadow-sm focus:ring-primary-500 focus:border-primary-500 block w-full sm:text-xs border-gray-200 dark:border-gray-700 rounded-md py-1.5" />
|
||||
</div>
|
||||
<div class="col-span-2">
|
||||
<button type="submit" class="w-full px-4 py-1.5 border border-transparent border-gray-200 dark:border-gray-700 text-xs font-medium rounded-md shadow-sm text-white dark:text-white bg-primary-500 hover:bg-primary-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500">{{ .locale.Tr "gist.search.placeholder.search" }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{{ end }}
|
||||
<div>
|
||||
{{ if ne (len .gists) 0 }}
|
||||
{{ range $gist := .gists }}
|
||||
|
8
templates/partials/_pagination.html
vendored
8
templates/partials/_pagination.html
vendored
@ -1,7 +1,7 @@
|
||||
{{ define "_pagination" }}
|
||||
<div class="flex justify-center space-x-2">
|
||||
{{ if .prevPage }}
|
||||
<a href="{{ $.c.ExternalUrl }}/{{ .urlPage }}?page={{ .prevPage }}{{ .urlParams }}" class="relative inline-flex items-center space-x-2 rounded-md border border-white dark:border-gray-900 bg-white dark:bg-gray-900 px-2 py-1.5 font-medium text-slate-700 dark:text-slate-300 hover:border-gray-200 dark:hover:border-gray-400 hover:text-slate-700 dark:hover:text-slate-300 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500 text-sm leading-4">
|
||||
{{ if .pagination.HasPrevious }}
|
||||
<a href="{{ $.c.ExternalUrl }}/{{ .urlPage }}{{ .pagination.PreviousURL }}" class="relative inline-flex items-center space-x-2 rounded-md border border-white dark:border-gray-900 bg-white dark:bg-gray-900 px-2 py-1.5 font-medium text-slate-700 dark:text-slate-300 hover:border-gray-200 dark:hover:border-gray-400 hover:text-slate-700 dark:hover:text-slate-300 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500 text-sm leading-4">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="mr-1 w-4 h-4">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 19.5L8.25 12l7.5-7.5" />
|
||||
</svg>
|
||||
@ -14,8 +14,8 @@
|
||||
</svg>
|
||||
{{ .prevLabel }}</span>
|
||||
{{ end }}
|
||||
{{ if .nextPage }}
|
||||
<a href="{{ $.c.ExternalUrl }}/{{ .urlPage }}?page={{ .nextPage }}{{ .urlParams }}" class="relative inline-flex items-center space-x-2 rounded-md border border-white dark:border-gray-900 bg-white dark:bg-gray-900 px-2 py-1.5 font-medium text-slate-700 dark:text-slate-300 hover:border-gray-200 dark:hover:border-gray-400 hover:text-slate-700 dark:hover:text-slate-300 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500 text-sm leading-4">{{ .nextLabel }}
|
||||
{{ if .pagination.HasNext }}
|
||||
<a href="{{ $.c.ExternalUrl }}/{{ .urlPage }}{{ .pagination.NextURL }}" class="relative inline-flex items-center space-x-2 rounded-md border border-white dark:border-gray-900 bg-white dark:bg-gray-900 px-2 py-1.5 font-medium text-slate-700 dark:text-slate-300 hover:border-gray-200 dark:hover:border-gray-400 hover:text-slate-700 dark:hover:text-slate-300 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500 text-sm leading-4">{{ .nextLabel }}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="ml-1 w-4 h-4">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5" />
|
||||
</svg>
|
||||
|
Loading…
x
Reference in New Issue
Block a user