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