Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

vole
Clone

Read only, no credentials:

https://rodent.software/git/vole.git

Write over SSH with a registered key:

git clone ssh://git@0.0.0.0:2222/vole.git

A free and open-source forge that lives in your Git repo.

Blame · server/web/ui.go

Line attribution on main · 130 line(s)

c717d4eb
Keenan Nemetz
13d ago
1 package web
2
753cd703
Keenan Nemetz
7d ago
3 // Presentation helpers that no single surface owns: relative time, breadcrumbs,
4 // path validation, and the page control the list pages share. Nothing here reads a store or decides anything about
c717d4eb
Keenan Nemetz
13d ago
5 // access; a value it returns is only ever printed.
6
7 import (
8 "fmt"
9 "strings"
10 "time"
11 )
12
13 func humanSince(t int64) string {
14 if t == 0 {
15 return ""
16 }
17 d := time.Since(time.Unix(t, 0))
18 switch {
19 case d < time.Minute:
20 return "just now"
21 case d < time.Hour:
22 return fmt.Sprintf("%dm ago", int(d.Minutes()))
23 case d < 24*time.Hour:
24 return fmt.Sprintf("%dh ago", int(d.Hours()))
25 default:
26 return fmt.Sprintf("%dd ago", int(d.Hours()/24))
27 }
28 }
29
30 func safePath(p string) bool {
31 if strings.Contains(p, "\x00") {
32 return false
33 }
34 for _, seg := range strings.Split(p, "/") {
35 if seg == ".." {
36 return false
37 }
38 }
39 return true
40 }
41
42 type crumb struct {
43 Name string
44 Path string
45 }
46
47 func crumbs(p string) []crumb {
48 var out []crumb
49 cur := ""
50 for _, seg := range strings.Split(p, "/") {
51 if seg == "" {
52 continue
53 }
54 if cur == "" {
55 cur = seg
56 } else {
57 cur += "/" + seg
58 }
59 out = append(out, crumb{Name: seg, Path: cur})
60 }
61 return out
62 }
63
31c03fc8
Keenan Nemetz
10d ago
64 // listPageSize is how many rows one page of an issue, change-request, or run list
c717d4eb
Keenan Nemetz
13d ago
65 // shows. The instance activity feed has its own, larger, page size (home.go).
66 const listPageSize = 25
67
68 // pager is one page of a list. The handler builds the URLs, so it keeps control
69 // of which filters survive; everything a template needs to draw the control is
70 // here, including the label and the fragment a page link swaps.
71 type pager struct {
72 // Label names the nav for a screen reader ("Issue pages").
73 Label string
306f36fc
Keenan Nemetz
12d ago
74 // Target is the HTMX selector a page link replaces. Empty navigates the
75 // whole page instead.
c717d4eb
Keenan Nemetz
13d ago
76 Target string
77 Page int
78 Pages int
79 Total int
80 Per int
81 From int
82 To int
83 PrevURL string
84 NextURL string
85 }
86
87 func newPager(total, per, page int, urlFor func(int) string) pager {
88 if per < 1 {
89 per = 1
90 }
91 pages := (total + per - 1) / per
92 if pages < 1 {
93 pages = 1
94 }
95 if page < 1 {
96 page = 1
97 }
98 if page > pages {
99 page = pages
100 }
101 p := pager{Page: page, Pages: pages, Total: total, Per: per}
102 if total > 0 {
103 p.From = (page-1)*per + 1
104 p.To = min(page*per, total)
105 }
106 if page > 1 {
107 p.PrevURL = urlFor(page - 1)
108 }
109 if page < pages {
110 p.NextURL = urlFor(page + 1)
111 }
112 return p
113 }
114
115 // forSwap names the control and the fragment it replaces, for a list that pages
116 // in place through HTMX. A list without it pages by navigation.
117 func (p pager) forSwap(label, target string) pager {
118 p.Label = label
119 p.Target = target
120 return p
121 }
122
123 // rows returns the half-open range of rows on this page, so a handler that has
124 // already collected and counted them does not repeat the arithmetic.
125 func (p pager) rows() (int, int) {
126 if p.Total == 0 {
127 return 0, 0
128 }
129 return (p.Page - 1) * p.Per, p.To
130 }