Files
ForgeBucket/internal/api/handlers/explore.go
T
erangel1 b624337b4a Debounced search bar — queries update 300ms after typing stops, clears with ✕ button
Repositories tab — lists all public repos as cards with owner/name link, description, default branch chip, last-updated time; sort by recently updated / newest / name A–Z; prev/next pagination
Users tab — grid of user cards with avatar/initials, username, join date; pagination
Skeleton loaders while fetching, opacity fade during page transitions
All state (tab, sort, query) reflected in the URL so links are shareable
2026-05-07 16:21:35 +02:00

128 lines
3.3 KiB
Go

package handlers
import (
"net/http"
"strconv"
"xorm.io/xorm"
"github.com/forgeo/forgebucket/internal/models"
)
type ExploreHandler struct{ db *xorm.Engine }
func NewExploreHandler(db *xorm.Engine) *ExploreHandler { return &ExploreHandler{db: db} }
type exploreRepo struct {
ID int64 `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
IsPrivate bool `json:"isPrivate"`
DefaultBranch string `json:"defaultBranch"`
CreatedAt string `json:"createdAt"`
UpdatedAt string `json:"updatedAt"`
OwnerID int64 `json:"ownerId"`
OwnerUsername string `json:"ownerUsername"`
OwnerAvatarURL string `json:"ownerAvatarUrl"`
}
type exploreUser struct {
ID int64 `json:"id"`
Username string `json:"username"`
AvatarURL string `json:"avatarUrl"`
CreatedAt string `json:"createdAt"`
}
func (h *ExploreHandler) Repos(w http.ResponseWriter, r *http.Request) {
q := r.URL.Query().Get("q")
sort := r.URL.Query().Get("sort")
limit := clampInt(r.URL.Query().Get("limit"), 20, 1, 50)
offset := clampInt(r.URL.Query().Get("offset"), 0, 0, 1<<30)
sess := h.db.Where("is_private = false")
if q != "" {
sess = sess.And("(name ILIKE ? OR description ILIKE ?)", "%"+q+"%", "%"+q+"%")
}
switch sort {
case "name":
sess = sess.Asc("name")
case "created":
sess = sess.Desc("created_at")
default:
sess = sess.Desc("updated_at")
}
var repos []models.Repository
if err := sess.Limit(limit, offset).Find(&repos); err != nil {
jsonError(w, "could not search repositories", http.StatusInternalServerError)
return
}
results := make([]exploreRepo, 0, len(repos))
for _, repo := range repos {
var owner models.User
found, _ := h.db.ID(repo.OwnerID).Cols("id", "username", "avatar_url").Get(&owner)
if !found {
continue
}
results = append(results, exploreRepo{
ID: repo.ID,
Name: repo.Name,
Description: repo.Description,
IsPrivate: repo.IsPrivate,
DefaultBranch: repo.DefaultBranch,
CreatedAt: repo.CreatedAt.Format("2006-01-02T15:04:05Z"),
UpdatedAt: repo.UpdatedAt.Format("2006-01-02T15:04:05Z"),
OwnerID: owner.ID,
OwnerUsername: owner.Username,
OwnerAvatarURL: owner.AvatarURL,
})
}
jsonOK(w, results)
}
func (h *ExploreHandler) Users(w http.ResponseWriter, r *http.Request) {
q := r.URL.Query().Get("q")
limit := clampInt(r.URL.Query().Get("limit"), 20, 1, 50)
offset := clampInt(r.URL.Query().Get("offset"), 0, 0, 1<<30)
sess := h.db.Cols("id", "username", "avatar_url", "created_at")
if q != "" {
sess = sess.Where("username ILIKE ?", "%"+q+"%")
} else {
sess = sess.Where("1 = 1")
}
sess = sess.Asc("username")
var users []models.User
if err := sess.Limit(limit, offset).Find(&users); err != nil {
jsonError(w, "could not search users", http.StatusInternalServerError)
return
}
results := make([]exploreUser, 0, len(users))
for _, u := range users {
results = append(results, exploreUser{
ID: u.ID,
Username: u.Username,
AvatarURL: u.AvatarURL,
CreatedAt: u.CreatedAt.Format("2006-01-02T15:04:05Z"),
})
}
jsonOK(w, results)
}
func clampInt(s string, def, min, max int) int {
v, err := strconv.Atoi(s)
if err != nil {
return def
}
if v < min {
return min
}
if v > max {
return max
}
return v
}