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}