1package web
  2
  3import (
  4	"html/template"
  5	"math"
  6	"net/http"
  7	"sort"
  8	"strconv"
  9	"strings"
 10	"time"
 11
 12	"github.com/charmbracelet/log/v2"
 13	"github.com/charmbracelet/soft-serve/pkg/access"
 14	"github.com/charmbracelet/soft-serve/pkg/backend"
 15	"github.com/charmbracelet/soft-serve/pkg/config"
 16	"github.com/charmbracelet/soft-serve/pkg/proto"
 17	"github.com/charmbracelet/soft-serve/pkg/ui/common"
 18	"github.com/dustin/go-humanize"
 19)
 20
 21type HomeRepository struct {
 22	Name        string
 23	ProjectName string
 24	Description string
 25	IsPrivate   bool
 26	CloneURL    string
 27	UpdatedAt   string
 28}
 29
 30type HomeData struct {
 31	Repo         proto.Repository
 32	Repositories []HomeRepository
 33	ReadmeHTML   template.HTML
 34	ActiveTab    string
 35	ServerName   string
 36	Page         int
 37	TotalPages   int
 38	HasPrevPage  bool
 39	HasNextPage  bool
 40}
 41
 42type repoItem struct {
 43	repo       proto.Repository
 44	lastUpdate *time.Time
 45}
 46
 47func home(w http.ResponseWriter, r *http.Request) {
 48	ctx := r.Context()
 49	logger := log.FromContext(ctx)
 50	cfg := config.FromContext(ctx)
 51	be := backend.FromContext(ctx)
 52
 53	if be == nil {
 54		logger.Debug("backend not found in context")
 55		renderInternalServerError(w, r)
 56		return
 57	}
 58
 59	repos, err := be.Repositories(ctx)
 60	if err != nil {
 61		logger.Debug("failed to get repositories", "err", err)
 62		renderInternalServerError(w, r)
 63		return
 64	}
 65
 66	var readmeHTML template.HTML
 67	homeRepos := make([]HomeRepository, 0)
 68	items := make([]repoItem, 0)
 69
 70	readmeHTML, err = getServerReadme(ctx, be)
 71	if err != nil {
 72		logger.Debug("failed to get server README", "err", err)
 73	}
 74
 75	for _, r := range repos {
 76		if r.IsHidden() {
 77			continue
 78		}
 79
 80		al := be.AccessLevelByPublicKey(ctx, r.Name(), nil)
 81		if al >= access.ReadOnlyAccess {
 82			var lastUpdate *time.Time
 83			lu := r.UpdatedAt()
 84			if !lu.IsZero() {
 85				lastUpdate = &lu
 86			}
 87			items = append(items, repoItem{
 88				repo:       r,
 89				lastUpdate: lastUpdate,
 90			})
 91		}
 92	}
 93
 94	sort.Slice(items, func(i, j int) bool {
 95		if items[i].lastUpdate == nil && items[j].lastUpdate != nil {
 96			return false
 97		}
 98		if items[i].lastUpdate != nil && items[j].lastUpdate == nil {
 99			return true
100		}
101		if items[i].lastUpdate == nil && items[j].lastUpdate == nil {
102			return items[i].repo.Name() < items[j].repo.Name()
103		}
104		return items[i].lastUpdate.After(*items[j].lastUpdate)
105	})
106
107	page := 1
108	if pageStr := r.URL.Query().Get("page"); pageStr != "" {
109		if p, err := strconv.Atoi(pageStr); err == nil && p > 0 {
110			page = p
111		}
112	}
113
114	totalRepos := len(items)
115	totalPages := int(math.Ceil(float64(totalRepos) / float64(defaultReposPerPage)))
116	if totalPages < 1 {
117		totalPages = 1
118	}
119
120	if page > totalPages {
121		page = totalPages
122	}
123	if page < 1 {
124		page = 1
125	}
126
127	startIdx := (page - 1) * defaultReposPerPage
128	endIdx := startIdx + defaultReposPerPage
129	if endIdx > totalRepos {
130		endIdx = totalRepos
131	}
132
133	paginatedItems := items[startIdx:endIdx]
134
135	for _, item := range paginatedItems {
136		repo := item.repo
137		name := repo.Name()
138		projectName := repo.ProjectName()
139		description := strings.TrimSpace(repo.Description())
140		cloneURL := common.RepoURL(cfg.SSH.PublicURL, name)
141
142		var updatedAt string
143		if item.lastUpdate != nil {
144			updatedAt = humanize.Time(*item.lastUpdate)
145		}
146
147		homeRepos = append(homeRepos, HomeRepository{
148			Name:        name,
149			ProjectName: projectName,
150			Description: description,
151			IsPrivate:   repo.IsPrivate(),
152			CloneURL:    cloneURL,
153			UpdatedAt:   updatedAt,
154		})
155	}
156
157	data := HomeData{
158		Repositories: homeRepos,
159		ReadmeHTML:   readmeHTML,
160		ActiveTab:    "repositories",
161		ServerName:   cfg.Name,
162		Page:         page,
163		TotalPages:   totalPages,
164		HasPrevPage:  page > 1,
165		HasNextPage:  page < totalPages,
166	}
167
168	renderHTML(w, "home.html", data)
169}