refactor(web): drop the projects page and dead helpers
10 files changed, +50 −122
- README.md modified +1 −1
- static/style.css modified +26 −26
- store.go modified +0 −6
- templates/index.html modified +9 −14
- templates/layout.html modified +2 −2
- templates/projects.html deleted +0 −24
- templates/search.html modified +1 −5
- templates/stars.html modified +1 −1
- templates/user.html modified +1 −1
- web.go modified +9 −42
README.md modified
| @@ -4,7 +4,7 @@ Small and lightweight git server for self hosting. Repositories are served over | ||
| 4 | 4 | |
| 5 | 5 | ## Features |
| 6 | 6 | |
| 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 | |
| 8 | 8 | - 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 |
| 9 | 9 | - 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 |
| 10 | 10 | - 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 @@ | ||
| 2 | 2 | @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')} |
| 3 | 3 | @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')} |
| 4 | 4 | @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} | |
| 6 | 6 | *{box-sizing:border-box} |
| 7 | 7 | 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)} |
| 8 | 8 | body{margin:0} |
| 9 | -a{color:var(--link);text-decoration:none} | |
| 9 | +a{color:var(--accent);text-decoration:none} | |
| 10 | 10 | a:hover{text-decoration:underline} |
| 11 | 11 | code,pre,.hash,.mode,.blob,.hunk,.code,textarea{font-family:"DejaVu Sans Mono",Menlo,Consolas,"Liberation Mono","SF Mono",monospace;font-size:13px} |
| 12 | 12 | 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} | ||
| 53 | 53 | .summary{margin-top:.2rem;display:grid;grid-template-columns:4fr 1.5fr 2.6fr;gap:2rem;margin-bottom:1.5rem;align-items:start} |
| 54 | 54 | .summary .event{background:var(--soft);padding:.4rem .5rem .5rem;margin-bottom:.6rem} |
| 55 | 55 | .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} | |
| 57 | 57 | .summary .event .hash{text-decoration:underline} |
| 58 | 58 | .summary .subject{margin:.3rem 0 0;background:none;border:0;padding:0;white-space:pre-wrap} |
| 59 | 59 | .summary h3{font-weight:400;font-size:1.25rem;margin:0 0 .5rem} |
| @@ -64,22 +64,20 @@ body.fluid main.file{padding-top:.9rem} | ||
| 64 | 64 | .readme{max-width:920px} |
| 65 | 65 | .readme>article>h1:first-child{margin-top:0} |
| 66 | 66 | @media(max-width:900px){.summary{grid-template-columns:1fr}} |
| 67 | -.descbar .inner{padding:.3rem 1rem} | |
| 68 | 67 | .repohead+main{margin-top:0} |
| 69 | 68 | main{padding-bottom:3rem} |
| 70 | -.cards{display:grid;grid-template-columns:repeat(3,1fr);gap:1.5rem;margin-bottom:1.5rem} | |
| 71 | 69 | .panel{background:var(--soft);padding:.8rem 1rem;margin-bottom:1.5rem} |
| 72 | 70 | .panel.narrow{max-width:34rem;margin:1rem auto} |
| 73 | 71 | .panel .btn{margin-top:.6rem} |
| 74 | 72 | .panel p{margin:.4rem 0 .6rem} |
| 75 | 73 | .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} |
| 76 | 74 | .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)} | |
| 79 | 77 | .btn.danger{background:#fff;color:var(--del);border-color:var(--del)} |
| 80 | 78 | .events .event{background:var(--soft);padding:.25rem .5rem .4rem;margin-bottom:.6rem;border-bottom:0} |
| 81 | 79 | .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)} | |
| 83 | 81 | .event p{margin:0;font-size:.95rem} |
| 84 | 82 | .columns{display:grid;grid-template-columns:minmax(0,1fr) 18rem;gap:2.5rem;align-items:start} |
| 85 | 83 | .columns.user{grid-template-columns:280px minmax(0,1fr);gap:2rem} |
| @@ -92,7 +90,7 @@ nav.tabs.small a.active{background:#ddd} | ||
| 92 | 90 | .profile p{margin:.2rem 0} |
| 93 | 91 | .profile .name{font-weight:600;font-size:1.35rem;line-height:1.25;margin:0} |
| 94 | 92 | .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} | |
| 96 | 94 | .avatar{object-fit:cover;border:1px solid var(--line);background:var(--soft)} |
| 97 | 95 | .bio{margin:.5rem 0 .6rem} |
| 98 | 96 | .bio p{margin:.3rem 0} |
| @@ -111,7 +109,7 @@ dl.meta dd{margin:0;overflow-wrap:anywhere} | ||
| 111 | 109 | .filebar .spacer{flex:1} |
| 112 | 110 | .filebar a.active{color:var(--fg);border-bottom:2px solid var(--fg);padding-bottom:.2rem} |
| 113 | 111 | .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} | |
| 115 | 113 | .filebar .mode{color:var(--muted);font-family:inherit;font-size:inherit} |
| 116 | 114 | .filebar .lastcommit{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:40rem} |
| 117 | 115 | .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 | ||
| 140 | 138 | table.blob{width:100%} |
| 141 | 139 | table.blob td,table.hunk td,.code td{padding:0 .5rem;white-space:pre;vertical-align:top} |
| 142 | 140 | 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} | |
| 144 | 142 | table.blob td.n{border-right:1px solid var(--bar);padding:0 .5rem 0 .6rem} |
| 145 | 143 | td.l{white-space:pre-wrap;overflow-wrap:anywhere;width:100%} |
| 146 | 144 | tr:target td{background:#fff8c5} |
| @@ -152,8 +150,8 @@ tr:target td{background:#fff8c5} | ||
| 152 | 150 | .code .lntd:first-child{min-height:70vh} |
| 153 | 151 | .code .lnlinks{text-decoration:underline} |
| 154 | 152 | .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} | |
| 157 | 155 | .code .lntd:last-child{padding-left:.6rem} |
| 158 | 156 | .code .line:target,.code .line:target .cl{background:#fff8c5} |
| 159 | 157 | table.hunk{width:100%;border:1px solid var(--line)} |
| @@ -174,7 +172,7 @@ form.stack small{color:var(--muted)} | ||
| 174 | 172 | .pair{display:grid;grid-template-columns:1fr 1fr;gap:1rem} |
| 175 | 173 | input,textarea{font:inherit;color:var(--fg);background:#fff;border:1px solid #ced4da;padding:.35rem .55rem} |
| 176 | 174 | 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)} | |
| 178 | 176 | input[type=file]{border:0;padding:0} |
| 179 | 177 | .actions{display:flex;justify-content:flex-end;gap:.5rem} |
| 180 | 178 | .actions.left{justify-content:flex-start} |
| @@ -185,7 +183,7 @@ input[type=file]{border:0;padding:0} | ||
| 185 | 183 | .avatarform small{display:block;color:var(--muted);margin-bottom:.5rem} |
| 186 | 184 | .cropper canvas{display:block;max-width:100%;border:1px solid var(--line);cursor:move;touch-action:none} |
| 187 | 185 | .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}} | |
| 189 | 187 | .twocol{display:grid;grid-template-columns:1fr 1fr;gap:2rem;margin-bottom:2rem} |
| 190 | 188 | .twocol label{display:flex;flex-direction:column;gap:.25rem;margin-bottom:.8rem} |
| 191 | 189 | .twocol .legend{display:block;margin-bottom:.4rem} |
| @@ -236,7 +234,7 @@ form.stack.centred input{width:100%} | ||
| 236 | 234 | .hits h4{margin:0 0 .2rem} |
| 237 | 235 | .hits table.blob{border:1px solid var(--line)} |
| 238 | 236 | 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} | |
| 240 | 238 | .hunk td.n a:hover{text-decoration:underline;font-weight:600} |
| 241 | 239 | .hunk tr.inline td{white-space:normal;padding:.4rem .6rem;background:#fff;border-top:1px solid var(--line);border-bottom:1px solid var(--line)} |
| 242 | 240 | .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} | ||
| 292 | 290 | .linkbutton{background:none;border:0;padding:0;font:inherit;color:var(--muted);cursor:pointer} |
| 293 | 291 | .linkbutton:hover{color:var(--del);text-decoration:underline} |
| 294 | 292 | 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} | |
| 296 | 294 | .landing{margin:0} |
| 297 | 295 | .landing>p{margin:.2rem 0 1rem} |
| 298 | 296 | .landing a.arrow{text-decoration:underline} |
| @@ -300,7 +298,7 @@ h3.featured{font-size:1.3rem;font-weight:400;margin:2rem 0 .5rem} | ||
| 300 | 298 | .featuredgrid{display:grid;grid-template-columns:1fr 1fr;gap:.9rem 1rem} |
| 301 | 299 | .feature{background:var(--soft);padding:.5rem .5rem .9rem} |
| 302 | 300 | .feature h4{margin:0 0 .4rem;font-weight:700} |
| 303 | -.feature h4 a{color:var(--repo)} | |
| 301 | +.feature h4 a{color:var(--accent)} | |
| 304 | 302 | .feature h4 a.owner{color:var(--muted)} |
| 305 | 303 | .feature blockquote{margin:0 0 .5rem;padding:0 0 0 .5rem;border-left:2px solid #aaa} |
| 306 | 304 | .feature .tags{margin:0} |
| @@ -334,7 +332,7 @@ pre.rangediff,pre.body{white-space:pre-wrap;overflow-wrap:anywhere} | ||
| 334 | 332 | .heathead{display:flex;justify-content:space-between;align-items:baseline;gap:.5rem;flex-wrap:wrap;margin-bottom:.3rem} |
| 335 | 333 | .years{display:flex;gap:.2rem;flex-wrap:wrap;font-size:.8rem} |
| 336 | 334 | .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} | |
| 338 | 336 | .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} |
| 339 | 337 | .heat .months{grid-column:2;grid-row:1;display:grid;grid-template-columns:repeat(var(--weeks),1fr);height:1.1rem} |
| 340 | 338 | .heat .months span{white-space:nowrap} |
| @@ -372,7 +370,7 @@ pre.rangediff,pre.body{white-space:pre-wrap;overflow-wrap:anywhere} | ||
| 372 | 370 | .sorts a.active{color:var(--fg);font-weight:600} |
| 373 | 371 | .hit{padding:.9rem 0;border-bottom:1px solid var(--line)} |
| 374 | 372 | .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)} | |
| 376 | 374 | .hit h4 a.owner{color:var(--muted)} |
| 377 | 375 | .hit p{margin:.15rem 0} |
| 378 | 376 | .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} | ||
| 385 | 383 | .langbar span{display:block;height:100%} |
| 386 | 384 | .langlist{list-style:none;margin:0 0 1.2rem;padding:0;font-size:.85rem;display:flex;flex-wrap:wrap;gap:.2rem 1rem} |
| 387 | 385 | .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} | |
| 390 | 388 | .facetbox h4{font-size:.75rem;text-transform:uppercase;letter-spacing:.05em;color:var(--muted);margin:.15rem .4rem .45rem;font-weight:600} |
| 391 | 389 | .facetlist{display:flex;flex-direction:column;gap:1px} |
| 392 | 390 | .facetlist a{display:flex;align-items:center;gap:.55rem;padding:.35rem .45rem;color:var(--fg);border-radius:4px;font-size:.93rem} |
| 393 | 391 | .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} | |
| 395 | 393 | .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)} | |
| 397 | 395 | .facetlist .dot{margin:0 .1rem 0 .15rem;width:.65rem;height:.65rem} |
| 398 | 396 | .facetlist .fname{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} |
| 399 | 397 | .facetlist .count{background:none;padding:0;color:var(--muted);font-size:.8rem;font-variant-numeric:tabular-nums;font-weight:400} |
| 400 | 398 | .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} | |
| 403 | 399 | @media(max-width:800px){.facetbox{border:0;padding:0;margin:0}.facetlist a{border-radius:14px}.facets{gap:.35rem}} |
| 404 | 400 | @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 { | ||
| 999 | 999 | return n == 1 |
| 1000 | 1000 | } |
| 1001 | 1001 | |
| 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 | - | |
| 1008 | 1002 | func (r Repo) Path() string { |
| 1009 | 1003 | return fmt.Sprintf("~%s/%s", r.Owner.Name, r.Name) |
| 1010 | 1004 | } |
templates/index.html modified
| @@ -1,27 +1,22 @@ | ||
| 1 | 1 | {{define "content"}} |
| 2 | 2 | {{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}} | |
| 7 | 6 | {{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> | |
| 11 | 7 | {{else}} |
| 12 | 8 | <div class="landing"> |
| 13 | 9 | <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> |
| 14 | 10 | <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> | |
| 17 | 13 | </div> |
| 18 | 14 | {{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" .}} | |
| 23 | 17 | </section>{{end}} |
| 18 | +</div>{{end}} | |
| 24 | 19 | {{end}} |
| 25 | 20 | {{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}} | |
| 27 | 22 | {{end}} |
templates/layout.html modified
| @@ -17,8 +17,8 @@ | ||
| 17 | 17 | <body{{if .Fluid}} class="fluid"{{end}}> |
| 18 | 18 | <header> |
| 19 | 19 | <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> | |
| 22 | 22 | {{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}} |
| 23 | 23 | <span class="spacer"></span> |
| 24 | 24 | {{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 @@ | ||
| 16 | 16 | <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> |
| 17 | 17 | {{with $d.Lang}}<a class="clearfacet" href="?q={{$d.Base}}&type={{$d.Type}}">× Clear {{.}}</a>{{end}} |
| 18 | 18 | </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> | |
| 23 | 19 | </aside> |
| 24 | 20 | <div class="results"> |
| 25 | 21 | {{if eq $d.Type "repos"}} |
| 26 | 22 | <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> |
| 27 | 23 | <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}} | |
| 29 | 25 | {{else}} |
| 30 | 26 | <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> |
| 31 | 27 | {{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 @@ | ||
| 3 | 3 | <h2>~{{.Data.Owner.Name}}</h2> |
| 4 | 4 | <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> |
| 5 | 5 | </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}} | |
| 7 | 7 | {{end}} |
templates/user.html modified
| @@ -20,7 +20,7 @@ | ||
| 20 | 20 | </aside> |
| 21 | 21 | <div class="main"> |
| 22 | 22 | {{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}} | |
| 24 | 24 | </div> |
| 25 | 25 | </div> |
| 26 | 26 | {{end}} |
web.go modified
| @@ -13,6 +13,7 @@ import ( | ||
| 13 | 13 | "log" |
| 14 | 14 | "mime" |
| 15 | 15 | "net/http" |
| 16 | + "net/url" | |
| 16 | 17 | "os" |
| 17 | 18 | "os/exec" |
| 18 | 19 | "path/filepath" |
| @@ -95,7 +96,6 @@ func newServer(store *Store, root, host, name string, open bool, sshUser string) | ||
| 95 | 96 | } |
| 96 | 97 | return "" |
| 97 | 98 | }, |
| 98 | - "mark": mark, | |
| 99 | 99 | "dict": func(kv ...any) map[string]any { |
| 100 | 100 | m := map[string]any{} |
| 101 | 101 | 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) | ||
| 105 | 105 | } |
| 106 | 106 | return m |
| 107 | 107 | }, |
| 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], "/") }, | |
| 110 | 109 | } |
| 111 | 110 | names, _ := fs.Glob(templateFS, "templates/*.html") |
| 112 | 111 | for _, name := range names { |
| @@ -243,17 +242,16 @@ func (s *Server) index(w http.ResponseWriter, r *http.Request) { | ||
| 243 | 242 | recent = s.byUpdate(recent) |
| 244 | 243 | featured, _ := s.store.FeaturedRepos() |
| 245 | 244 | if len(featured) == 0 { |
| 246 | - featured = recent[:min(len(recent), 6)] | |
| 245 | + featured = slices.Clone(recent[:min(len(recent), 6)]) | |
| 247 | 246 | } |
| 248 | - recent = recent[:min(len(recent), 20)] | |
| 249 | 247 | var repos, shared []Repo |
| 250 | 248 | if user != nil { |
| 251 | 249 | repos, _ = s.store.ReposByOwner(user.ID, true) |
| 252 | 250 | shared, _ = s.store.SharedRepos(user.ID) |
| 253 | 251 | repos, shared = s.byUpdate(repos), s.byUpdate(shared) |
| 252 | + recent = slices.DeleteFunc(recent, func(r Repo) bool { return r.Owner.ID == user.ID }) | |
| 254 | 253 | } |
| 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}}) | |
| 257 | 255 | } |
| 258 | 256 | |
| 259 | 257 | func (s *Server) loginForm(w http.ResponseWriter, r *http.Request) { |
| @@ -465,19 +463,11 @@ func visibility(v string) int { | ||
| 465 | 463 | } |
| 466 | 464 | |
| 467 | 465 | 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) | |
| 478 | 469 | } |
| 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) | |
| 481 | 471 | } |
| 482 | 472 | |
| 483 | 473 | func (s *Server) byUpdate(repos []Repo) []Repo { |
| @@ -1372,29 +1362,6 @@ func ago(t time.Time) string { | ||
| 1372 | 1362 | } |
| 1373 | 1363 | } |
| 1374 | 1364 | |
| 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 | - | |
| 1398 | 1365 | func perm(mode string) string { |
| 1399 | 1366 | bits := func(o string) string { |
| 1400 | 1367 | n, _ := strconv.ParseInt(o, 8, 16) |