~gitpocket/gitpocket

refactor(web): drop projects page and dead helpers, simplify logo, unify palette around muted indigo

commit
f7c873c7c4451f0795f0c261be37d1afa410e092 browse tree
parent
bf99c38bd0dcf74114d5b49712c59d0c0ef9149e
author
james <j@cpl0.zip>
signature
Verified ~james signed with a key registered by ~james
date
2026-11-13 10:42 UTC

10 files changed, +50 −122

README.md modified

@@ -4,7 +4,7 @@ Small and lightweight git server for self hosting. Repositories are served over
44
55 ## Features
66
7-- Repos sit under `~user/name` and can be public or unlisted or private. Each one takes up to three tags, and there is a `/projects` page that searches across all of them sorted by whoever pushed most recently
7+- Repos sit under `~user/name` and can be public or unlisted or private. Each one takes up to three tags, and the old `/projects` address now redirects to the search page with the matching tag qualifier
88 - Tree and blob and log and commit and refs views, plus a case insensitive search across file contents at any ref backed by `git grep`. Chroma does the highlighting and goldmark rendering the Markdown before bluemonday cleans it. Anything in `$x$` or `$$x$$` gets typeset in the browser
99 - The last commit shown against every tree entry comes out of one `git log --name-only` walk per directory instead of a separate process per file
1010 - Profiles have a display name and a Markdown bio along with location and workplace and website, plus an avatar that you crop in the browser and the server then resamples down to a 256px PNG

static/style.css modified

@@ -2,11 +2,11 @@
22 @font-face{font-family:"Source Sans 3";font-style:italic;font-weight:400;font-display:swap;src:url(/static/fonts/sans-400-italic.woff2) format('woff2')}
33 @font-face{font-family:"Source Sans 3";font-style:normal;font-weight:600;font-display:swap;src:url(/static/fonts/sans-600-normal.woff2) format('woff2')}
44 @font-face{font-family:"Source Sans 3";font-style:normal;font-weight:700;font-display:swap;src:url(/static/fonts/sans-700-normal.woff2) format('woff2')}
5-:root{--heat0:#ebedf0;--heat1:#c3c8cf;--heat2:#868e96;--heat3:#495057;--heat4:#16181b;--fg:#212529;--muted:#6c757d;--line:#dee2e6;--soft:#f8f9fa;--bar:#ddd;--link:#0640e0;--primary:#0047ab;--repo:#0047ab;--add:#1a7f37;--del:#c0392b;--addbg:#e6ffec;--delbg:#ffebe9;--code:#f8f9fa}
5+:root{--accent:#44467d;--accentdark:#35366b;--accentsoft:#f3f4fd;--focus:#c4c8e8;--heat0:#ebedf0;--heat1:#c4c8e8;--heat2:#959acb;--heat3:#6a6ea8;--heat4:#44467d;--fg:#212529;--muted:#6c757d;--line:#dee2e6;--soft:#f8f9fa;--bar:#ddd;--add:#1a7f37;--del:#c0392b;--addbg:#e6ffec;--delbg:#ffebe9;--code:#f8f9fa}
66 *{box-sizing:border-box}
77 html{font:15px/1.5 system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue","Noto Sans","DejaVu Sans",Arial,sans-serif;background:#fff;color:var(--fg)}
88 body{margin:0}
9-a{color:var(--link);text-decoration:none}
9+a{color:var(--accent);text-decoration:none}
1010 a:hover{text-decoration:underline}
1111 code,pre,.hash,.mode,.blob,.hunk,.code,textarea{font-family:"DejaVu Sans Mono",Menlo,Consolas,"Liberation Mono","SF Mono",monospace;font-size:13px}
1212 pre{background:var(--code);padding:.6rem .8rem;overflow-x:auto;border:1px solid var(--line)}
@@ -53,7 +53,7 @@ body.fluid main.file{padding-top:.9rem}
5353 .summary{margin-top:.2rem;display:grid;grid-template-columns:4fr 1.5fr 2.6fr;gap:2rem;margin-bottom:1.5rem;align-items:start}
5454 .summary .event{background:var(--soft);padding:.4rem .5rem .5rem;margin-bottom:.6rem}
5555 .summary .event .row{display:flex;justify-content:space-between;gap:1rem}
56-.summary .event .when{font-size:.85rem;color:var(--link);text-decoration:underline}
56+.summary .event .when{font-size:.85rem;color:var(--accent);text-decoration:underline}
5757 .summary .event .hash{text-decoration:underline}
5858 .summary .subject{margin:.3rem 0 0;background:none;border:0;padding:0;white-space:pre-wrap}
5959 .summary h3{font-weight:400;font-size:1.25rem;margin:0 0 .5rem}
@@ -64,22 +64,20 @@ body.fluid main.file{padding-top:.9rem}
6464 .readme{max-width:920px}
6565 .readme>article>h1:first-child{margin-top:0}
6666 @media(max-width:900px){.summary{grid-template-columns:1fr}}
67-.descbar .inner{padding:.3rem 1rem}
6867 .repohead+main{margin-top:0}
6968 main{padding-bottom:3rem}
70-.cards{display:grid;grid-template-columns:repeat(3,1fr);gap:1.5rem;margin-bottom:1.5rem}
7169 .panel{background:var(--soft);padding:.8rem 1rem;margin-bottom:1.5rem}
7270 .panel.narrow{max-width:34rem;margin:1rem auto}
7371 .panel .btn{margin-top:.6rem}
7472 .panel p{margin:.4rem 0 .6rem}
7573 .btn{display:inline-block;font:inherit;font-size:.9rem;background:#e9ecef;color:var(--fg);border:1px solid #ccc;padding:.25rem .7rem;cursor:pointer;line-height:1.4}
7674 .btn:hover{text-decoration:none;background:#dde1e5}
77-.btn.primary{background:var(--primary);color:#fff;border-color:var(--primary)}
78-.btn.primary:hover{background:#003a8c;border-color:#003a8c}
75+.btn.primary{background:var(--accent);color:#fff;border-color:var(--accent)}
76+.btn.primary:hover{background:var(--accentdark);border-color:var(--accentdark)}
7977 .btn.danger{background:#fff;color:var(--del);border-color:var(--del)}
8078 .events .event{background:var(--soft);padding:.25rem .5rem .4rem;margin-bottom:.6rem;border-bottom:0}
8179 .event h4{font-size:1rem;font-weight:400;margin:0 0 .2rem;padding-bottom:.15rem;border-bottom:1px solid var(--line)}
82-.event h4 a{color:var(--repo)}
80+.event h4 a{color:var(--accent)}
8381 .event p{margin:0;font-size:.95rem}
8482 .columns{display:grid;grid-template-columns:minmax(0,1fr) 18rem;gap:2.5rem;align-items:start}
8583 .columns.user{grid-template-columns:280px minmax(0,1fr);gap:2rem}
@@ -92,7 +90,7 @@ nav.tabs.small a.active{background:#ddd}
9290 .profile p{margin:.2rem 0}
9391 .profile .name{font-weight:600;font-size:1.35rem;line-height:1.25;margin:0}
9492 .profile .btn.block{display:block;text-align:center;margin-top:.45rem}
95-.info{background:#d1ecf1;color:#0c5460;padding:.5rem .6rem;border:1px solid #bee5eb;margin:0}
93+.info{background:var(--accentsoft);color:var(--fg);padding:.5rem .7rem;border:1px solid var(--focus);margin:0}
9694 .avatar{object-fit:cover;border:1px solid var(--line);background:var(--soft)}
9795 .bio{margin:.5rem 0 .6rem}
9896 .bio p{margin:.3rem 0}
@@ -111,7 +109,7 @@ dl.meta dd{margin:0;overflow-wrap:anywhere}
111109 .filebar .spacer{flex:1}
112110 .filebar a.active{color:var(--fg);border-bottom:2px solid var(--fg);padding-bottom:.2rem}
113111 .filebar a{color:var(--muted)}
114-.filebar .crumbs a{color:var(--link);text-decoration:underline}
112+.filebar .crumbs a{color:var(--accent);text-decoration:underline}
115113 .filebar .mode{color:var(--muted);font-family:inherit;font-size:inherit}
116114 .filebar .lastcommit{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:40rem}
117115 .readme,.bio,.comment .readme{font-family:"Source Sans 3",system-ui,sans-serif;font-size:17px;line-height:1.55}
@@ -140,7 +138,7 @@ table.refs td,table.keys td{padding:.3rem 1rem .3rem 0;border-bottom:1px solid v
140138 table.blob{width:100%}
141139 table.blob td,table.hunk td,.code td{padding:0 .5rem;white-space:pre;vertical-align:top}
142140 td.n{text-align:right;color:var(--muted);user-select:none;width:1%}
143-td.n a{color:var(--link);text-decoration:underline}
141+td.n a{color:var(--accent);text-decoration:underline}
144142 table.blob td.n{border-right:1px solid var(--bar);padding:0 .5rem 0 .6rem}
145143 td.l{white-space:pre-wrap;overflow-wrap:anywhere;width:100%}
146144 tr:target td{background:#fff8c5}
@@ -152,8 +150,8 @@ tr:target td{background:#fff8c5}
152150 .code .lntd:first-child{min-height:70vh}
153151 .code .lnlinks{text-decoration:underline}
154152 .code .lnt{padding-right:.5rem}
155-.code .lnt,.code .ln{color:var(--link);margin-right:0;padding:0 .5rem 0 .4rem}
156-.code .lntd a{color:var(--link);text-decoration:none}
153+.code .lnt,.code .ln{color:var(--accent);margin-right:0;padding:0 .5rem 0 .4rem}
154+.code .lntd a{color:var(--accent);text-decoration:none}
157155 .code .lntd:last-child{padding-left:.6rem}
158156 .code .line:target,.code .line:target .cl{background:#fff8c5}
159157 table.hunk{width:100%;border:1px solid var(--line)}
@@ -174,7 +172,7 @@ form.stack small{color:var(--muted)}
174172 .pair{display:grid;grid-template-columns:1fr 1fr;gap:1rem}
175173 input,textarea{font:inherit;color:var(--fg);background:#fff;border:1px solid #ced4da;padding:.35rem .55rem}
176174 input:disabled{background:#e9ecef}
177-input:focus,textarea:focus{outline:2px solid #b6d4fe;border-color:#86b7fe}
175+input:focus,textarea:focus{outline:2px solid var(--focus);border-color:var(--accent)}
178176 input[type=file]{border:0;padding:0}
179177 .actions{display:flex;justify-content:flex-end;gap:.5rem}
180178 .actions.left{justify-content:flex-start}
@@ -185,7 +183,7 @@ input[type=file]{border:0;padding:0}
185183 .avatarform small{display:block;color:var(--muted);margin-bottom:.5rem}
186184 .cropper canvas{display:block;max-width:100%;border:1px solid var(--line);cursor:move;touch-action:none}
187185 .cropper input[type=range]{width:100%;padding:0}
188-@media(max-width:800px){.cards,.cards.commits,.columns,.columns.user,.pair,.avatarform{grid-template-columns:1fr}.profile .avatar{width:120px;height:120px}}
186+@media(max-width:800px){.columns,.columns.user,.pair,.avatarform{grid-template-columns:1fr}.profile .avatar{width:120px;height:120px}}
189187 .twocol{display:grid;grid-template-columns:1fr 1fr;gap:2rem;margin-bottom:2rem}
190188 .twocol label{display:flex;flex-direction:column;gap:.25rem;margin-bottom:.8rem}
191189 .twocol .legend{display:block;margin-bottom:.4rem}
@@ -236,7 +234,7 @@ form.stack.centred input{width:100%}
236234 .hits h4{margin:0 0 .2rem}
237235 .hits table.blob{border:1px solid var(--line)}
238236 mark{background:#fff3b0;padding:0}
239-.hunk td.n a{color:var(--link);text-decoration:none}
237+.hunk td.n a{color:var(--accent);text-decoration:none}
240238 .hunk td.n a:hover{text-decoration:underline;font-weight:600}
241239 .hunk tr.inline td{white-space:normal;padding:.4rem .6rem;background:#fff;border-top:1px solid var(--line);border-bottom:1px solid var(--line)}
242240 .hunk tr.inline .comment{margin:0;max-width:60rem}
@@ -292,7 +290,7 @@ a.label{opacity:.75}a.label.on,a.label:hover{opacity:1;text-decoration:none}
292290 .linkbutton{background:none;border:0;padding:0;font:inherit;color:var(--muted);cursor:pointer}
293291 .linkbutton:hover{color:var(--del);text-decoration:underline}
294292 nav.site .brand{display:inline-flex;align-items:center;gap:.4rem}
295-.logo{width:1.75rem;height:1.75rem;fill:#fff}.logo .pocket{fill:var(--fg)}.logo .branch{fill:none;stroke:#fff;stroke-width:1.5;stroke-linecap:round}
293+.logo{width:1.5rem;height:1.5rem;fill:var(--accent)}.logo circle{fill:#fff}
296294 .landing{margin:0}
297295 .landing>p{margin:.2rem 0 1rem}
298296 .landing a.arrow{text-decoration:underline}
@@ -300,7 +298,7 @@ h3.featured{font-size:1.3rem;font-weight:400;margin:2rem 0 .5rem}
300298 .featuredgrid{display:grid;grid-template-columns:1fr 1fr;gap:.9rem 1rem}
301299 .feature{background:var(--soft);padding:.5rem .5rem .9rem}
302300 .feature h4{margin:0 0 .4rem;font-weight:700}
303-.feature h4 a{color:var(--repo)}
301+.feature h4 a{color:var(--accent)}
304302 .feature h4 a.owner{color:var(--muted)}
305303 .feature blockquote{margin:0 0 .5rem;padding:0 0 0 .5rem;border-left:2px solid #aaa}
306304 .feature .tags{margin:0}
@@ -334,7 +332,7 @@ pre.rangediff,pre.body{white-space:pre-wrap;overflow-wrap:anywhere}
334332 .heathead{display:flex;justify-content:space-between;align-items:baseline;gap:.5rem;flex-wrap:wrap;margin-bottom:.3rem}
335333 .years{display:flex;gap:.2rem;flex-wrap:wrap;font-size:.8rem}
336334 .years a{color:var(--muted);padding:0 .35rem}
337-.years a.active{background:var(--fg);color:#fff}
335+.years a.active{background:var(--accent);color:#fff}
338336 .heat{display:grid;grid-template-columns:2rem 1fr;grid-template-rows:auto auto;font-size:9px;color:var(--muted);font-family:system-ui,sans-serif}
339337 .heat .months{grid-column:2;grid-row:1;display:grid;grid-template-columns:repeat(var(--weeks),1fr);height:1.1rem}
340338 .heat .months span{white-space:nowrap}
@@ -372,7 +370,7 @@ pre.rangediff,pre.body{white-space:pre-wrap;overflow-wrap:anywhere}
372370 .sorts a.active{color:var(--fg);font-weight:600}
373371 .hit{padding:.9rem 0;border-bottom:1px solid var(--line)}
374372 .hit h4{margin:0 0 .25rem;font-size:1.05rem;font-weight:600}
375-.hit h4 a{color:var(--repo)}
373+.hit h4 a{color:var(--accent)}
376374 .hit h4 a.owner{color:var(--muted)}
377375 .hit p{margin:.15rem 0}
378376 .hit .tags a{color:var(--muted);text-decoration:underline;margin-right:.6rem;font-size:.85rem}
@@ -385,20 +383,22 @@ pre.rangediff,pre.body{white-space:pre-wrap;overflow-wrap:anywhere}
385383 .langbar span{display:block;height:100%}
386384 .langlist{list-style:none;margin:0 0 1.2rem;padding:0;font-size:.85rem;display:flex;flex-wrap:wrap;gap:.2rem 1rem}
387385 .langlist .pct{color:var(--muted);margin-left:.25rem}
388-@media(max-width:800px){.bigsearch{margin-left:0}.searchpage{grid-template-columns:1fr;gap:.6rem}.navsearch{order:5;flex-basis:100%;margin:0}.facets{display:flex;flex-direction:column;gap:.5rem}.facetbox{padding:.4rem}.facetbox h4,.facetbox.tips{display:none}.facetlist{flex-direction:row;flex-wrap:wrap;gap:.35rem}.facetlist a{border:1px solid var(--line);padding:.2rem .55rem}.facetlist a.active{border-color:var(--repo);box-shadow:none}}
389-.facetbox{border:1px solid var(--line);border-radius:6px;padding:.5rem;margin-bottom:1rem}
386+@media(max-width:800px){.bigsearch{margin-left:0}.searchpage{grid-template-columns:1fr;gap:.6rem}.navsearch{order:5;flex-basis:100%;margin:0}.facets{display:flex;flex-direction:column;gap:.5rem}.facetbox{padding:.4rem}.facetbox h4{display:none}.facetlist{flex-direction:row;flex-wrap:wrap;gap:.35rem}.facetlist a{border:1px solid var(--line);padding:.2rem .55rem}.facetlist a.active{border-color:var(--accent);box-shadow:none}}
387+.facetbox{margin-bottom:1.4rem}
390388 .facetbox h4{font-size:.75rem;text-transform:uppercase;letter-spacing:.05em;color:var(--muted);margin:.15rem .4rem .45rem;font-weight:600}
391389 .facetlist{display:flex;flex-direction:column;gap:1px}
392390 .facetlist a{display:flex;align-items:center;gap:.55rem;padding:.35rem .45rem;color:var(--fg);border-radius:4px;font-size:.93rem}
393391 .facetlist a:hover{background:var(--soft);text-decoration:none}
394-.facetlist a.active{background:#e8eef8;color:var(--repo);font-weight:600}
392+.facetlist a.active{background:var(--accentsoft);color:var(--accent);font-weight:600;box-shadow:inset 2px 0 0 var(--accent);border-radius:0 4px 4px 0}
395393 .facetlist svg{width:16px;height:16px;flex:none;fill:none;stroke:currentColor;stroke-width:1.4;stroke-linecap:round;stroke-linejoin:round;color:var(--muted)}
396-.facetlist a.active svg{color:var(--repo)}
394+.facetlist a.active svg{color:var(--accent)}
397395 .facetlist .dot{margin:0 .1rem 0 .15rem;width:.65rem;height:.65rem}
398396 .facetlist .fname{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
399397 .facetlist .count{background:none;padding:0;color:var(--muted);font-size:.8rem;font-variant-numeric:tabular-nums;font-weight:400}
400398 .clearfacet{display:block;font-size:.8rem;color:var(--muted);margin:.4rem .45rem .1rem}
401-.facetbox.tips p{margin:0 .4rem .2rem;display:flex;flex-wrap:wrap;gap:.3rem}
402-.facetbox.tips code{font-size:.75rem;background:var(--soft);padding:.05rem .3rem;border-radius:3px}
403399 @media(max-width:800px){.facetbox{border:0;padding:0;margin:0}.facetlist a{border-radius:14px}.facets{gap:.35rem}}
404400 @media(max-width:800px){.facetlist{flex-direction:row;flex-wrap:wrap;gap:.35rem}.facetlist .fname{flex:none}.facetlist a{gap:.4rem}.facets{flex-direction:row;flex-wrap:wrap}}
401+.dash{display:grid;grid-template-columns:1fr;gap:2.5rem;align-items:start;max-width:52rem}.dash:has(>section+section){grid-template-columns:3fr 2fr;max-width:none}
402+.dashhead{display:flex;justify-content:space-between;align-items:center;border-bottom:1px solid var(--line);margin-bottom:.6rem;padding-bottom:.3rem}
403+.dashhead h3{border:0;margin:0;padding:0}
404+@media(max-width:800px){.dash{grid-template-columns:1fr}}

store.go modified

@@ -999,12 +999,6 @@ func (s *Store) UseInvite(code string) bool {
999999 return n == 1
10001000 }
10011001
1002-func (s *Store) Stats() (users, repos int) {
1003- s.db.QueryRow(`select count(*) from users`).Scan(&users)
1004- s.db.QueryRow(`select count(*) from repos where private = 0`).Scan(&repos)
1005- return
1006-}
1007-
10081002 func (r Repo) Path() string {
10091003 return fmt.Sprintf("~%s/%s", r.Owner.Name, r.Name)
10101004 }

templates/index.html modified

@@ -1,27 +1,22 @@
11 {{define "content"}}
22 {{if .User}}
3-<div class="cards">
4-<section class="panel"><h3>Your repositories</h3>
5-{{if .Data.Repos}}{{template "repolist" .Data.Repos}}{{else}}<p>You have no repositories yet.</p>{{end}}
6-<a class="btn primary" href="/create">Create a repository ▸</a>
3+<div class="dash">
4+<section><div class="dashhead"><h3>Your repositories</h3><a class="btn primary" href="/create">New repository</a></div>
5+{{if .Data.Repos}}{{template "repolist" .Data.Repos}}{{else}}<p class="muted">You have no repositories yet.</p>{{end}}
76 {{if .Data.Shared}}<h3>Shared with you</h3>{{template "repolist" .Data.Shared}}{{end}}</section>
8-<section class="panel"><h3>Your profile</h3><p>Set a bio in Markdown, a location, a workplace and an avatar. Your page is <a href="/~{{.User.Name}}">~{{.User.Name}}</a>.</p><a class="btn" href="/settings">Edit your profile ▸</a></section>
9-<section class="panel"><h3>Keys and tokens</h3><p>Register SSH public keys for pushing and create access tokens for HTTPS pushes or scripts.</p><a class="btn" href="/settings/keys">Manage keys ▸</a></section>
10-</div>
117 {{else}}
128 <div class="landing">
139 <p><a class="btn primary" href="/register">Register here ▸</a> or <a class="arrow" href="/login">Log in ▸</a> to get started, or check out some of our featured projects:</p>
1410 <h3 class="featured">Featured projects</h3>
15-<div class="featuredgrid">{{range .Data.Featured}}<div class="feature"><h4><a class="owner" href="/~{{.Owner.Name}}">~{{.Owner.Name}}</a>/<a href="/{{.Path}}">{{.Name}}</a></h4>{{if .Description}}<blockquote>{{.Description}}</blockquote>{{end}}{{if .Tags}}<p class="tags">{{range .Tags}}<a href="/projects?tag={{.}}">#{{.}}</a>{{end}}</p>{{end}}</div>{{end}}</div>
16-<div class="more"><a class="btn" href="/projects">More projects ▸</a></div>
11+<div class="featuredgrid">{{range .Data.Featured}}<div class="feature"><h4><a class="owner" href="/~{{.Owner.Name}}">~{{.Owner.Name}}</a>/<a href="/{{.Path}}">{{.Name}}</a></h4>{{if .Description}}<blockquote>{{.Description}}</blockquote>{{end}}{{if .Tags}}<p class="tags">{{range .Tags}}<a href="/search?q=tag:{{.}}">#{{.}}</a>{{end}}</p>{{end}}</div>{{end}}</div>
12+<div class="more"><a class="btn" href="/search">More projects ▸</a></div>
1713 </div>
1814 {{end}}
19-{{if .User}}<section class="panel wide" id="recent">
20-<h3>Recent repositories</h3>
21-<p>{{.Data.Public}} public repositories from {{.Data.Users}} users.</p>
22-{{template "repolist" .Data.Recent}}
15+{{if .User}}{{with .Data.Recent}}<section><h3>Recently pushed elsewhere</h3>
16+{{template "repolist" .}}
2317 </section>{{end}}
18+</div>{{end}}
2419 {{end}}
2520 {{define "repolist"}}
26-{{if .}}<div class="events">{{range .}}<div class="event"><h4><a href="/{{.Path}}">{{.Path}}</a>{{if .Private}} <span class="tag">private</span>{{else if .Unlisted}} <span class="tag">unlisted</span>{{end}}{{if .Stars}}<span class="starcount" title="{{.Stars}} stars">☆ {{.Stars}}</span>{{end}}</h4>{{if .Description}}<p>{{.Description}}</p>{{end}}{{if .Tags}}<p class="tags">{{range .Tags}}<a href="/projects?tag={{.}}">#{{.}}</a> {{end}}</p>{{end}}</div>{{end}}</div>{{else}}<p class="muted">Nothing here yet.</p>{{end}}
21+{{if .}}<div class="events">{{range .}}<div class="event"><h4><a href="/{{.Path}}">{{.Path}}</a>{{if .Private}} <span class="tag">private</span>{{else if .Unlisted}} <span class="tag">unlisted</span>{{end}}{{if .Stars}}<span class="starcount" title="{{.Stars}} stars">☆ {{.Stars}}</span>{{end}}</h4>{{if .Description}}<p>{{.Description}}</p>{{end}}{{if .Tags}}<p class="tags">{{range .Tags}}<a href="/search?q=tag:{{.}}">#{{.}}</a> {{end}}</p>{{end}}</div>{{end}}</div>{{else}}<p class="muted">Nothing here yet.</p>{{end}}
2722 {{end}}

templates/layout.html modified

@@ -17,8 +17,8 @@
1717 <body{{if .Fluid}} class="fluid"{{end}}>
1818 <header>
1919 <nav class="site">
20-<a class="brand" href="/"><svg class="logo" viewBox="0 0 24 24" aria-hidden="true"><path class="pocket" d="M3.5 3.5h17v9.5c0 4.4-3.8 8-8.5 8s-8.5-3.6-8.5-8z"/><circle cx="9" cy="9" r="1.6"/><circle cx="9" cy="16" r="1.6"/><circle cx="15" cy="11.5" r="1.6"/><path class="branch" d="M9 10.6v3.8M15 13.1c0 1.6-2.2 2.2-4.4 2.6"/></svg>{{.Name}}</a>
21-<a href="/projects">projects</a>
20+<a class="brand" href="/"><svg class="logo" viewBox="0 0 24 24" aria-hidden="true"><path d="M4 7a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v5a8 8 0 0 1-16 0z"/><circle cx="12" cy="11" r="2.4"/></svg>{{.Name}}</a>
21+<a href="/search">explore</a>
2222 {{if not (and .Data (index .Data "Facets"))}}<form class="navsearch" method="get" action="/search" role="search"><input name="q" placeholder="Search repositories, people, languages" aria-label="Search"></form>{{end}}
2323 <span class="spacer"></span>
2424 {{if .User}}<span>Logged in as <a href="/~{{.User.Name}}">~{{.User.Name}}</a> — <form method="post" action="/logout"><button>Log out</button></form></span>{{else}}<span class="auth"><a href="/login">Log in</a> — <a href="/register">Register</a></span>{{end}}

templates/projects.html deleted

@@ -1,24 +0,0 @@
1-{{define "content"}}
2-<div class="columns browse">
3-<div class="main">
4-<h3 class="plain">Browse public projects</h3>
5-<form method="get" action="/projects" class="search">
6-<input name="search" value="{{.Data.Query}}" placeholder="Search all public projects">
7-<div class="sort">
8-<span>Sort results by</span>
9-<label><input type="radio" name="sort" value="updated"{{if ne .Data.Sort "created"}} checked{{end}}> Recently updated</label>
10-<label><input type="radio" name="sort" value="created"{{if eq .Data.Sort "created"}} checked{{end}}> Recently created</label>
11-{{if .Data.Tag}}<input type="hidden" name="tag" value="{{.Data.Tag}}">{{end}}
12-<button class="btn">Apply ▸</button>
13-</div>
14-</form>
15-{{if .Data.Tag}}<p class="muted">Showing projects tagged <a href="/projects?tag={{.Data.Tag}}">#{{.Data.Tag}}</a> · <a href="/projects">clear</a></p>{{end}}
16-{{if .Data.Repos}}<div class="events">{{range .Data.Repos}}<div class="event"><h4><a href="/~{{.Owner.Name}}">~{{.Owner.Name}}</a>/<a href="/{{.Path}}">{{.Name}}</a>{{if .Stars}}<span class="starcount" title="{{.Stars}} stars">☆ {{.Stars}}</span>{{end}}</h4>{{if .Description}}<p>{{.Description}}</p>{{end}}{{if .Tags}}<p class="tags">{{range .Tags}}<a href="/projects?tag={{.}}">#{{.}}</a> {{end}}</p>{{end}}</div>{{end}}</div>{{else}}<p class="info">No projects match.</p>{{end}}
17-</div>
18-<aside>
19-<h3 class="plain">About</h3>
20-<p>{{.Data.Public}} public projects from {{.Data.Users}} users on {{.Host}}.</p>
21-{{if .User}}<a class="btn block" href="/create">Create a new repository ▸</a>{{else}}<a class="btn block" href="/register">Register ▸</a>{{end}}
22-</aside>
23-</div>
24-{{end}}

templates/search.html modified

@@ -16,16 +16,12 @@
1616 <nav class="facetlist">{{range $d.Languages}}<a href="?q={{.Query}}&type={{$d.Type}}"{{if eq .Name $d.Lang}} class="active"{{end}}><i class="dot" style="background:{{.Colour}}"></i><span class="fname">{{.Name}}</span><span class="count">{{.Count}}</span></a>{{end}}</nav>
1717 {{with $d.Lang}}<a class="clearfacet" href="?q={{$d.Base}}&type={{$d.Type}}">× Clear {{.}}</a>{{end}}
1818 </section>{{end}}
19-<section class="facetbox tips">
20-<h4>Qualifiers</h4>
21-<p><code>language:ocaml</code> <code>user:james</code> <code>tag:sel4</code></p>
22-</section>
2319 </aside>
2420 <div class="results">
2521 {{if eq $d.Type "repos"}}
2622 <div class="resulthead"><h3 class="plain">{{len $d.Repos}} repositor{{if eq (len $d.Repos) 1}}y{{else}}ies{{end}}{{with $d.Query}} matching <q>{{.}}</q>{{end}}</h3>
2723 <nav class="sorts"><a href="?q={{$d.Query}}&type=repos{{with $d.Lang}}&lang={{.}}{{end}}"{{if not $d.Sort}} class="active"{{end}}>Best match</a><a href="?q={{$d.Query}}&type=repos&sort=updated{{with $d.Lang}}&lang={{.}}{{end}}"{{if eq $d.Sort "updated"}} class="active"{{end}}>Recently updated</a><a href="?q={{$d.Query}}&type=repos&sort=stars{{with $d.Lang}}&lang={{.}}{{end}}"{{if eq $d.Sort "stars"}} class="active"{{end}}>Most stars</a></nav></div>
28-{{range $d.Repos}}<div class="hit"><h4><a class="owner" href="/~{{.Owner.Name}}">~{{.Owner.Name}}</a>/<a href="/{{.Path}}">{{.Name}}</a></h4>{{with .Description}}<p>{{.}}</p>{{end}}{{if .Tags}}<p class="tags">{{range .Tags}}<a href="/projects?tag={{.}}">#{{.}}</a>{{end}}</p>{{end}}<p class="meta">{{with .Language}}<span><i class="dot" style="background:{{.Colour}}"></i>{{.Name}}</span>{{end}}{{if .Stars}}<span>☆ {{.Stars}}</span>{{end}}<span>Updated {{ago .Updated}}</span></p></div>{{else}}<p class="info">No repositories match. Try a shorter query or clear the language filter.</p>{{end}}
24+{{range $d.Repos}}<div class="hit"><h4><a class="owner" href="/~{{.Owner.Name}}">~{{.Owner.Name}}</a>/<a href="/{{.Path}}">{{.Name}}</a></h4>{{with .Description}}<p>{{.}}</p>{{end}}{{if .Tags}}<p class="tags">{{range .Tags}}<a href="/search?q=tag:{{.}}">#{{.}}</a>{{end}}</p>{{end}}<p class="meta">{{with .Language}}<span><i class="dot" style="background:{{.Colour}}"></i>{{.Name}}</span>{{end}}{{if .Stars}}<span>☆ {{.Stars}}</span>{{end}}<span>Updated {{ago .Updated}}</span></p></div>{{else}}<p class="info">No repositories match. Try a shorter query or clear the language filter.</p>{{end}}
2925 {{else}}
3026 <div class="resulthead"><h3 class="plain">{{len $d.Users}} user{{if ne (len $d.Users) 1}}s{{end}}{{with $d.Query}} matching <q>{{.}}</q>{{end}}{{with $d.Lang}} writing {{.}}{{end}}</h3></div>
3127 {{range $d.Users}}<div class="hit user"><a href="/~{{.Name}}" class="hitavatar">{{if .Profile.Avatar}}<img src="/~{{.Name}}/avatar?v={{.Profile.Avatar}}" alt="">{{else}}<span>{{slice .Name 0 1}}</span>{{end}}</a><div><h4><a href="/~{{.Name}}">{{with .Profile.Display}}{{.}}{{else}}~{{.Name}}{{end}}</a> <span class="muted">~{{.Name}}</span></h4>{{with .Profile.Bio}}<p class="bio">{{.}}</p>{{end}}<p class="meta">{{with .Profile.Location}}<span>{{.}}</span>{{end}}<span>{{.Repos}} public repositor{{if eq .Repos 1}}y{{else}}ies{{end}}</span><span>Joined {{.Created.Format "January 2006"}}</span></p></div></div>{{else}}<p class="info">No users match.</p>{{end}}

templates/stars.html modified

@@ -3,5 +3,5 @@
33 <h2>~{{.Data.Owner.Name}}</h2>
44 <nav class="tabs small"><a href="/~{{.Data.Owner.Name}}">about</a><a class="active" href="/~{{.Data.Owner.Name}}/stars">stars{{if .Data.Count}} <span class="count">{{.Data.Count}}</span>{{end}}</a></nav>
55 </div>
6-{{if .Data.Starred}}<div class="events">{{range .Data.Starred}}<div class="event"><h4><a href="/~{{.Owner.Name}}">~{{.Owner.Name}}</a>/<a href="/{{.Path}}">{{.Name}}</a>{{if .Stars}}<span class="starcount" title="{{.Stars}} stars">☆ {{.Stars}}</span>{{end}}</h4>{{if .Description}}<p>{{.Description}}</p>{{end}}{{if .Tags}}<p class="tags">{{range .Tags}}<a href="/projects?tag={{.}}">#{{.}}</a> {{end}}</p>{{end}}</div>{{end}}</div>{{else}}<p class="info">~{{.Data.Owner.Name}} has not starred anything yet.</p>{{end}}
6+{{if .Data.Starred}}<div class="events">{{range .Data.Starred}}<div class="event"><h4><a href="/~{{.Owner.Name}}">~{{.Owner.Name}}</a>/<a href="/{{.Path}}">{{.Name}}</a>{{if .Stars}}<span class="starcount" title="{{.Stars}} stars">☆ {{.Stars}}</span>{{end}}</h4>{{if .Description}}<p>{{.Description}}</p>{{end}}{{if .Tags}}<p class="tags">{{range .Tags}}<a href="/search?q=tag:{{.}}">#{{.}}</a> {{end}}</p>{{end}}</div>{{end}}</div>{{else}}<p class="info">~{{.Data.Owner.Name}} has not starred anything yet.</p>{{end}}
77 {{end}}

templates/user.html modified

@@ -20,7 +20,7 @@
2020 </aside>
2121 <div class="main">
2222 {{with .Data.Activity}}<section class="activity"><div class="heathead"><p class="muted small">{{.Total}} commit{{if ne .Total 1}}s{{end}} {{if .Year}}in {{.Year}}{{else}}in the past 12 months{{end}}</p><nav class="years"><a href="?"{{if not .Year}} class="active"{{end}}>past 12 months</a>{{range .Years}}<a href="?year={{.}}"{{if eq . $.Data.Activity.Year}} class="active"{{end}}>{{.}}</a>{{end}}</nav></div>{{.Grid}}<p class="legend muted small">less <i class="h0"></i><i class="h1"></i><i class="h2"></i><i class="h3"></i><i class="h4"></i> more</p></section>{{end}}
23-{{if .Data.Repos}}<div class="events">{{range .Data.Repos}}<div class="event"><h4><a href="/{{.Path}}">{{.Name}}</a>{{if .Private}} <span class="tag">private</span>{{else if .Unlisted}} <span class="tag">unlisted</span>{{end}}{{if .Stars}}<span class="starcount" title="{{.Stars}} stars">☆ {{.Stars}}</span>{{end}}</h4>{{if .Description}}<p>{{.Description}}</p>{{end}}{{if .Tags}}<p class="tags">{{range .Tags}}<a href="/projects?tag={{.}}">#{{.}}</a> {{end}}</p>{{end}}</div>{{end}}</div>{{else}}<p class="info">This user does not have any repositories.</p>{{end}}
23+{{if .Data.Repos}}<div class="events">{{range .Data.Repos}}<div class="event"><h4><a href="/{{.Path}}">{{.Name}}</a>{{if .Private}} <span class="tag">private</span>{{else if .Unlisted}} <span class="tag">unlisted</span>{{end}}{{if .Stars}}<span class="starcount" title="{{.Stars}} stars">☆ {{.Stars}}</span>{{end}}</h4>{{if .Description}}<p>{{.Description}}</p>{{end}}{{if .Tags}}<p class="tags">{{range .Tags}}<a href="/search?q=tag:{{.}}">#{{.}}</a> {{end}}</p>{{end}}</div>{{end}}</div>{{else}}<p class="info">This user does not have any repositories.</p>{{end}}
2424 </div>
2525 </div>
2626 {{end}}

web.go modified

@@ -13,6 +13,7 @@ import (
1313 "log"
1414 "mime"
1515 "net/http"
16+ "net/url"
1617 "os"
1718 "os/exec"
1819 "path/filepath"
@@ -95,7 +96,6 @@ func newServer(store *Store, root, host, name string, open bool, sshUser string)
9596 }
9697 return ""
9798 },
98- "mark": mark,
9999 "dict": func(kv ...any) map[string]any {
100100 m := map[string]any{}
101101 for i := 0; i+1 < len(kv); i += 2 {
@@ -105,8 +105,7 @@ func newServer(store *Store, root, host, name string, open bool, sshUser string)
105105 }
106106 return m
107107 },
108- "join": func(parts []string, upto int) string { return strings.Join(parts[:upto], "/") },
109- "string": func(b []byte) string { return string(b) },
108+ "join": func(parts []string, upto int) string { return strings.Join(parts[:upto], "/") },
110109 }
111110 names, _ := fs.Glob(templateFS, "templates/*.html")
112111 for _, name := range names {
@@ -243,17 +242,16 @@ func (s *Server) index(w http.ResponseWriter, r *http.Request) {
243242 recent = s.byUpdate(recent)
244243 featured, _ := s.store.FeaturedRepos()
245244 if len(featured) == 0 {
246- featured = recent[:min(len(recent), 6)]
245+ featured = slices.Clone(recent[:min(len(recent), 6)])
247246 }
248- recent = recent[:min(len(recent), 20)]
249247 var repos, shared []Repo
250248 if user != nil {
251249 repos, _ = s.store.ReposByOwner(user.ID, true)
252250 shared, _ = s.store.SharedRepos(user.ID)
253251 repos, shared = s.byUpdate(repos), s.byUpdate(shared)
252+ recent = slices.DeleteFunc(recent, func(r Repo) bool { return r.Owner.ID == user.ID })
254253 }
255- users, public := s.store.Stats()
256- s.render(w, r, "index", page{Title: s.host, User: user, Data: map[string]any{"Repos": repos, "Shared": shared, "Recent": recent, "Featured": featured, "Users": users, "Public": public}})
254+ s.render(w, r, "index", page{Title: s.host, User: user, Data: map[string]any{"Repos": repos, "Shared": shared, "Recent": recent[:min(len(recent), 10)], "Featured": featured}})
257255 }
258256
259257 func (s *Server) loginForm(w http.ResponseWriter, r *http.Request) {
@@ -465,19 +463,11 @@ func visibility(v string) int {
465463 }
466464
467465 func (s *Server) projects(w http.ResponseWriter, r *http.Request) {
468- query := strings.TrimSpace(r.URL.Query().Get("search"))
469- tag := strings.TrimSpace(r.URL.Query().Get("tag"))
470- sort := r.URL.Query().Get("sort")
471- repos, err := s.store.PublicRepos(query, tag)
472- if err != nil {
473- s.fail(w, r, 500, err.Error())
474- return
475- }
476- if sort != "created" {
477- repos = s.byUpdate(repos)
466+ q := strings.TrimSpace(r.URL.Query().Get("search"))
467+ if tag := strings.TrimSpace(r.URL.Query().Get("tag")); tag != "" {
468+ q = strings.TrimSpace(q + " tag:" + tag)
478469 }
479- users, public := s.store.Stats()
480- s.render(w, r, "projects", page{Title: "Browse public projects", Data: map[string]any{"Repos": repos, "Query": query, "Tag": tag, "Sort": sort, "Users": users, "Public": public}})
470+ http.Redirect(w, r, "/search?q="+url.QueryEscape(q), http.StatusMovedPermanently)
481471 }
482472
483473 func (s *Server) byUpdate(repos []Repo) []Repo {
@@ -1372,29 +1362,6 @@ func ago(t time.Time) string {
13721362 }
13731363 }
13741364
1375-func mark(text, query string) template.HTML {
1376- if query == "" {
1377- return template.HTML(template.HTMLEscapeString(text))
1378- }
1379- var b strings.Builder
1380- lower := strings.ToLower(text)
1381- q := strings.ToLower(query)
1382- for {
1383- i := strings.Index(lower, q)
1384- if i < 0 {
1385- b.WriteString(template.HTMLEscapeString(text))
1386- break
1387- }
1388- b.WriteString(template.HTMLEscapeString(text[:i]))
1389- b.WriteString("<mark>")
1390- b.WriteString(template.HTMLEscapeString(text[i : i+len(q)]))
1391- b.WriteString("</mark>")
1392- text = text[i+len(q):]
1393- lower = lower[i+len(q):]
1394- }
1395- return template.HTML(b.String())
1396-}
1397-
13981365 func perm(mode string) string {
13991366 bits := func(o string) string {
14001367 n, _ := strconv.ParseInt(o, 8, 16)