b624337b4a
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
128 lines
3.3 KiB
Go
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
|
|
}
|