feat(org): let members edit org profile, picture and membership
5 files changed, +116 −22
- static/style.css modified +6 −10
- templates/org.html modified +3 −3
- templates/orgsettings.html added +37 −0
- templates/search.html modified +1 −1
- web.go modified +69 −8
static/style.css modified
| @@ -403,25 +403,18 @@ pre.rangediff,pre.body{white-space:pre-wrap;overflow-wrap:anywhere} | ||
| 403 | 403 | .dashhead h3{border:0;margin:0;padding:0} |
| 404 | 404 | @media(max-width:800px){.dash{grid-template-columns:1fr}} |
| 405 | 405 | .orghead{display:flex;align-items:flex-start;gap:1.4rem;margin:-1rem 0 1.6rem;padding:1.4rem 1.5rem;background:var(--accentsoft);border-left:4px solid var(--accent)} |
| 406 | -.orghead .btn{margin-left:auto;flex:none} | |
| 406 | +.orgactions{margin-left:auto;flex:none;display:flex;flex-direction:column;gap:.45rem}.orgactions .btn{text-align:center} | |
| 407 | 407 | .orgmark img,.orgmark span{display:flex;width:5.5rem;height:5.5rem;border-radius:2px;object-fit:cover} |
| 408 | 408 | .orgmark span,.people li span,.orglist span{align-items:center;justify-content:center;background:var(--accent);color:#fff;font-weight:600;text-transform:uppercase} |
| 409 | 409 | .orgmark span{font-size:2.4rem} |
| 410 | 410 | .orgident{min-width:0} |
| 411 | -.orgident h2{display:flex;align-items:center;flex-wrap:wrap;gap:.6rem;font-size:1.7rem;margin:0} | |
| 411 | +.orgident h2{font-size:1.7rem;margin:0} | |
| 412 | 412 | .orgident .handle{color:var(--muted);margin:.1rem 0 .5rem} |
| 413 | 413 | .orgident .bio{margin:0 0 .5rem;max-width:44rem} |
| 414 | 414 | .orgident .bio p{margin:0} |
| 415 | -.orgbadge{display:inline-block;border:1px solid var(--accent);color:var(--accent);border-radius:3px;padding:0 .4rem;font-size:.72rem;font-weight:600;letter-spacing:.04em;text-transform:uppercase;vertical-align:middle;line-height:1.5} | |
| 416 | 415 | .facts.inline{display:flex;flex-wrap:wrap;gap:.2rem 1.2rem;margin:0} |
| 417 | 416 | .columns.org{grid-template-columns:minmax(0,1fr) 220px;gap:2rem} |
| 418 | 417 | .columns.org h3 .count,.people h3 .count{color:var(--muted);font-weight:400;font-size:.9rem;margin-left:.3rem} |
| 419 | -.orgrepos{display:grid;grid-template-columns:repeat(auto-fill,minmax(16rem,1fr));gap:.7rem;margin-bottom:1.6rem} | |
| 420 | -.orgrepo{display:flex;flex-direction:column;gap:.3rem;padding:.8rem .9rem;border:1px solid var(--line);border-top:3px solid var(--accent);color:var(--fg);text-decoration:none} | |
| 421 | -.orgrepo:hover{background:var(--accentsoft)} | |
| 422 | -.orgrepo strong{color:var(--accent);font-size:1.05rem} | |
| 423 | -.orgrepo .desc{font-size:.93rem} | |
| 424 | -.orgrepo .meta{margin-top:auto;color:var(--muted);font-size:.82rem} | |
| 425 | 418 | .people ul{list-style:none;margin:0;padding:0} |
| 426 | 419 | .people li a{display:grid;grid-template-columns:2.4rem 1fr;grid-template-rows:auto auto;column-gap:.6rem;align-items:center;padding:.35rem 0;color:var(--fg);text-decoration:none} |
| 427 | 420 | .people li img,.people li span{grid-row:1/3;display:flex;width:2.4rem;height:2.4rem;border-radius:50%;object-fit:cover} |
| @@ -432,4 +425,7 @@ pre.rangediff,pre.body{white-space:pre-wrap;overflow-wrap:anywhere} | ||
| 432 | 425 | .orglist a{display:flex;align-items:center;gap:.5rem;color:var(--fg);margin:.3rem 0} |
| 433 | 426 | .orglist img,.orglist span{display:flex;width:1.6rem;height:1.6rem;border-radius:2px;object-fit:cover;font-size:.8rem} |
| 434 | 427 | .hit.org .hitavatar span{background:var(--accent);color:#fff;border-radius:2px} |
| 435 | -@media (max-width:800px){.orghead{flex-wrap:wrap;padding:1rem}.orghead .btn{margin-left:0}.columns.org{grid-template-columns:1fr}} | |
| 428 | +@media (max-width:800px){.orghead{flex-wrap:wrap;padding:1rem}.orgactions{margin-left:0;flex-direction:row}.columns.org{grid-template-columns:1fr}} | |
| 429 | +.memberlist{list-style:none;margin:0 0 .8rem;padding:0;max-width:32rem} | |
| 430 | +.memberlist li{display:flex;align-items:center;justify-content:space-between;padding:.35rem 0;border-bottom:1px solid var(--line)} | |
| 431 | +.memberlist form{margin:0} | |
templates/org.html modified
| @@ -4,7 +4,7 @@ | ||
| 4 | 4 | <header class="orghead"> |
| 5 | 5 | <div class="orgmark">{{if $p.Avatar}}<img src="/~{{$o.Name}}/avatar?v={{$p.Avatar}}" alt="">{{else}}<span>{{slice $o.Name 0 1}}</span>{{end}}</div> |
| 6 | 6 | <div class="orgident"> |
| 7 | -<h2>{{with $p.Display}}{{.}}{{else}}{{$o.Name}}{{end}} <span class="orgbadge">Organisation</span></h2> | |
| 7 | +<h2>{{with $p.Display}}{{.}}{{else}}{{$o.Name}}{{end}}</h2> | |
| 8 | 8 | <p class="handle">~{{$o.Name}}</p> |
| 9 | 9 | {{with .Data.Bio}}<div class="bio readme">{{.}}</div>{{end}} |
| 10 | 10 | <ul class="facts inline"> |
| @@ -13,12 +13,12 @@ | ||
| 13 | 13 | <li title="{{date $o.Created}}"><svg viewBox="0 0 16 16" aria-hidden="true"><rect x="2" y="3" width="12" height="11" rx="1.5"/><path d="M2 6.5h12M5.5 1.5v3M10.5 1.5v3"/></svg>Since {{$o.Created.Format "January 2006"}}</li> |
| 14 | 14 | </ul> |
| 15 | 15 | </div> |
| 16 | -{{if .Data.Member}}<a class="btn primary" href="/create?owner={{$o.Name}}">New repository</a>{{end}} | |
| 16 | +{{if .Data.Member}}<div class="orgactions"><a class="btn primary" href="/create?owner={{$o.Name}}">New repository</a><a class="btn" href="/~{{$o.Name}}/settings">Edit organisation</a></div>{{end}} | |
| 17 | 17 | </header> |
| 18 | 18 | <div class="columns org"> |
| 19 | 19 | <div class="main"> |
| 20 | 20 | <h3 class="plain">Repositories <span class="count">{{len .Data.Repos}}</span></h3> |
| 21 | -{{if .Data.Repos}}<div class="orgrepos">{{range .Data.Repos}}<a class="orgrepo" href="/{{.Path}}"><strong>{{.Name}}</strong>{{if .Private}} <span class="tag">private</span>{{else if .Unlisted}} <span class="tag">unlisted</span>{{end}}{{if .Description}}<span class="desc">{{.Description}}</span>{{end}}<span class="meta">Updated {{ago .Updated}}{{if .Stars}} · ☆ {{.Stars}}{{end}}</span></a>{{end}}</div>{{else}}<p class="info">This organisation does not have any repositories.</p>{{end}} | |
| 21 | +{{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 organisation does not have any repositories.</p>{{end}} | |
| 22 | 22 | {{with .Data.Activity}}<section class="activity"><div class="heathead"><p class="muted small">{{.Total}} commit{{if ne .Total 1}}s{{end}} across the organisation {{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 | 23 | </div> |
| 24 | 24 | <aside class="people"> |
templates/orgsettings.html added
| @@ -0,0 +1,37 @@ | ||
| 1 | +{{define "content"}} | |
| 2 | +{{$p := .Data.Profile}} | |
| 3 | +{{$o := .Data.Org}} | |
| 4 | +<h3 class="rule">Edit <a href="/~{{$o.Name}}">~{{$o.Name}}</a></h3> | |
| 5 | +<form method="post" action="/settings/avatar" enctype="multipart/form-data" class="avatarform" id="avatarform"> | |
| 6 | +<input type="hidden" name="org" value="{{$o.Name}}"> | |
| 7 | +<div class="avatarbox">{{if $p.Avatar}}<img class="avatar" src="/~{{$o.Name}}/avatar?v={{$p.Avatar}}" alt="">{{end}}</div> | |
| 8 | +<div> | |
| 9 | +<label>Choose a picture <input type="file" name="avatar" accept="image/png,image/jpeg,image/gif,image/webp" required></label> | |
| 10 | +<small>If the image is not square you can pick the crop below. It is resized to 256×256 and stored as PNG.</small> | |
| 11 | +<div class="cropper" hidden><canvas></canvas><input type="range" min="10" max="100" value="100"><small>Drag to move the square, scroll or slide to resize.</small></div> | |
| 12 | +<input type="hidden" name="x"><input type="hidden" name="y"><input type="hidden" name="size"> | |
| 13 | +<div class="actions left"><button class="btn">Upload picture ▸</button>{{if $p.Avatar}}<button class="btn danger" formaction="/settings/avatar/delete" formnovalidate>Remove</button>{{end}}</div> | |
| 14 | +</div> | |
| 15 | +</form> | |
| 16 | +<script src="/static/crop.js" defer></script> | |
| 17 | +<h3 class="rule">Profile</h3> | |
| 18 | +<form method="post" action="/settings/profile" class="stack wide"> | |
| 19 | +<input type="hidden" name="org" value="{{$o.Name}}"> | |
| 20 | +<div class="pair"> | |
| 21 | +<label>Handle <span class="muted">(you can't edit this)</span><input value="{{$o.Name}}" disabled></label> | |
| 22 | +<label>Name <input name="display" value="{{$p.Display}}" maxlength="100" placeholder="Shown on the organisation page"></label> | |
| 23 | +</div> | |
| 24 | +<div class="pair"> | |
| 25 | +<label>Location <input name="location" value="{{$p.Location}}" maxlength="100"></label> | |
| 26 | +<label>URL <input name="website" value="{{$p.Website}}" maxlength="200" placeholder="https://"></label> | |
| 27 | +</div> | |
| 28 | +<label>Bio <textarea name="bio" rows="5" maxlength="4000" placeholder="Markdown supported">{{$p.Bio}}</textarea></label> | |
| 29 | +<div class="actions"><button class="btn primary">Save changes ▸</button></div> | |
| 30 | +</form> | |
| 31 | +<h3 class="rule">Members</h3> | |
| 32 | +<p class="muted small">Members can push to, configure and create repositories under ~{{$o.Name}}, and edit this page.</p> | |
| 33 | +<ul class="memberlist">{{range .Data.Members}}<li><a href="/~{{.Name}}">{{with .Profile.Display}}{{.}} {{end}}<span class="muted">~{{.Name}}</span></a><form method="post" action="/~{{$o.Name}}/members"><input type="hidden" name="name" value="{{.Name}}"><button class="btn danger small" name="remove" value="1">Remove</button></form></li>{{end}}</ul> | |
| 34 | +<form method="post" action="/~{{$o.Name}}/members" class="inline"> | |
| 35 | +<input name="name" placeholder="username" required> <button class="btn">Add member ▸</button> | |
| 36 | +</form> | |
| 37 | +{{end}} | |
templates/search.html modified
| @@ -24,7 +24,7 @@ | ||
| 24 | 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}} |
| 25 | 25 | {{else}} |
| 26 | 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> |
| 27 | -{{range $d.Users}}<div class="hit user{{if .Org}} org{{end}}"><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>{{if .Org}} <span class="orgbadge">Organisation</span>{{end}}</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>{{if .Org}}Since{{else}}Joined{{end}} {{.Created.Format "January 2006"}}</span></p></div></div>{{else}}<p class="info">No users match.</p>{{end}} | |
| 27 | +{{range $d.Users}}<div class="hit user{{if .Org}} org{{end}}"><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>{{if .Org}}Since{{else}}Joined{{end}} {{.Created.Format "January 2006"}}</span></p></div></div>{{else}}<p class="info">No users match.</p>{{end}} | |
| 28 | 28 | {{end}} |
| 29 | 29 | </div> |
| 30 | 30 | </div> |
web.go modified
| @@ -592,8 +592,25 @@ func (s *Server) endSessions(w http.ResponseWriter, r *http.Request) { | ||
| 592 | 592 | http.Redirect(w, r, "/login", http.StatusSeeOther) |
| 593 | 593 | } |
| 594 | 594 | |
| 595 | -func (s *Server) profile(w http.ResponseWriter, r *http.Request) { | |
| 595 | +func (s *Server) subject(w http.ResponseWriter, r *http.Request) (*User, string) { | |
| 596 | 596 | u := s.require(w, r) |
| 597 | + if u == nil { | |
| 598 | + return nil, "" | |
| 599 | + } | |
| 600 | + name := r.FormValue("org") | |
| 601 | + if name == "" { | |
| 602 | + return u, "/settings" | |
| 603 | + } | |
| 604 | + org, err := s.store.UserByName(name) | |
| 605 | + if err != nil || !s.store.IsOrg(org.ID) || !s.store.IsMember(org.ID, u.ID) { | |
| 606 | + s.fail(w, r, 403, "You are not a member of that organisation") | |
| 607 | + return nil, "" | |
| 608 | + } | |
| 609 | + return &org, "/~" + org.Name + "/settings" | |
| 610 | +} | |
| 611 | + | |
| 612 | +func (s *Server) profile(w http.ResponseWriter, r *http.Request) { | |
| 613 | + u, back := s.subject(w, r) | |
| 597 | 614 | if u == nil { |
| 598 | 615 | return |
| 599 | 616 | } |
| @@ -606,7 +623,7 @@ func (s *Server) profile(w http.ResponseWriter, r *http.Request) { | ||
| 606 | 623 | } |
| 607 | 624 | p := Profile{Display: field("display", 100), Bio: field("bio", 4000), Location: field("location", 100), Workplace: field("workplace", 100), Website: field("website", 200)} |
| 608 | 625 | if err := s.store.UpdateProfile(u.ID, p); err != nil { |
| 609 | - http.Redirect(w, r, "/settings?error="+template.URLQueryEscaper(err.Error()), http.StatusSeeOther) | |
| 626 | + http.Redirect(w, r, back+"?error="+template.URLQueryEscaper(err.Error()), http.StatusSeeOther) | |
| 610 | 627 | return |
| 611 | 628 | } |
| 612 | 629 | http.Redirect(w, r, "/~"+u.Name, http.StatusSeeOther) |
| @@ -617,20 +634,20 @@ func (s *Server) avatarPath(id int64) string { | ||
| 617 | 634 | } |
| 618 | 635 | |
| 619 | 636 | func (s *Server) avatar(w http.ResponseWriter, r *http.Request) { |
| 620 | - u := s.require(w, r) | |
| 637 | + u, back := s.subject(w, r) | |
| 621 | 638 | if u == nil { |
| 622 | 639 | return |
| 623 | 640 | } |
| 624 | 641 | r.Body = http.MaxBytesReader(w, r.Body, 16<<20) |
| 625 | 642 | f, _, err := r.FormFile("avatar") |
| 626 | 643 | if err != nil { |
| 627 | - http.Redirect(w, r, "/settings?error=Choose+an+image+under+16+MiB", http.StatusSeeOther) | |
| 644 | + http.Redirect(w, r, back+"?error=Choose+an+image+under+16+MiB", http.StatusSeeOther) | |
| 628 | 645 | return |
| 629 | 646 | } |
| 630 | 647 | defer f.Close() |
| 631 | 648 | data, err := io.ReadAll(f) |
| 632 | 649 | if err != nil { |
| 633 | - http.Redirect(w, r, "/settings?error=Choose+an+image+under+16+MiB", http.StatusSeeOther) | |
| 650 | + http.Redirect(w, r, back+"?error=Choose+an+image+under+16+MiB", http.StatusSeeOther) | |
| 634 | 651 | return |
| 635 | 652 | } |
| 636 | 653 | x, _ := strconv.Atoi(r.FormValue("x")) |
| @@ -643,7 +660,7 @@ func (s *Server) avatar(w http.ResponseWriter, r *http.Request) { | ||
| 643 | 660 | } |
| 644 | 661 | } |
| 645 | 662 | if err != nil { |
| 646 | - http.Redirect(w, r, "/settings?error="+template.URLQueryEscaper(err.Error()), http.StatusSeeOther) | |
| 663 | + http.Redirect(w, r, back+"?error="+template.URLQueryEscaper(err.Error()), http.StatusSeeOther) | |
| 647 | 664 | return |
| 648 | 665 | } |
| 649 | 666 | s.store.SetAvatar(u.ID, time.Now().Unix()) |
| @@ -651,13 +668,13 @@ func (s *Server) avatar(w http.ResponseWriter, r *http.Request) { | ||
| 651 | 668 | } |
| 652 | 669 | |
| 653 | 670 | func (s *Server) deleteAvatar(w http.ResponseWriter, r *http.Request) { |
| 654 | - u := s.require(w, r) | |
| 671 | + u, back := s.subject(w, r) | |
| 655 | 672 | if u == nil { |
| 656 | 673 | return |
| 657 | 674 | } |
| 658 | 675 | os.Remove(s.avatarPath(u.ID)) |
| 659 | 676 | s.store.SetAvatar(u.ID, 0) |
| 660 | - http.Redirect(w, r, "/settings", http.StatusSeeOther) | |
| 677 | + http.Redirect(w, r, back, http.StatusSeeOther) | |
| 661 | 678 | } |
| 662 | 679 | |
| 663 | 680 | func (s *Server) serveAvatar(w http.ResponseWriter, r *http.Request, owner User) { |
| @@ -798,6 +815,10 @@ func (s *Server) route(w http.ResponseWriter, r *http.Request) { | ||
| 798 | 815 | s.userFeed(w, r, owner) |
| 799 | 816 | return |
| 800 | 817 | } |
| 818 | + if (name == "settings" || name == "members") && len(parts) == 2 && s.store.IsOrg(owner.ID) { | |
| 819 | + s.orgSettings(w, r, owner, name) | |
| 820 | + return | |
| 821 | + } | |
| 801 | 822 | if name == "stars" && len(parts) == 2 && r.Method == "GET" { |
| 802 | 823 | starred, _ := s.store.StarredRepos(owner.ID) |
| 803 | 824 | s.render(w, r, "stars", page{Title: "~" + owner.Name + " stars", User: user, Data: map[string]any{"Owner": owner, "Starred": starred, "Count": len(starred)}}) |
| @@ -1422,3 +1443,43 @@ func humanSize(n int64) string { | ||
| 1422 | 1443 | return fmt.Sprintf("%.1f MiB", float64(n)/(1<<20)) |
| 1423 | 1444 | } |
| 1424 | 1445 | } |
| 1446 | + | |
| 1447 | +func (s *Server) orgSettings(w http.ResponseWriter, r *http.Request, org User, action string) { | |
| 1448 | + u := s.require(w, r) | |
| 1449 | + if u == nil { | |
| 1450 | + return | |
| 1451 | + } | |
| 1452 | + if !s.store.IsMember(org.ID, u.ID) { | |
| 1453 | + s.fail(w, r, 403, "You are not a member of this organisation") | |
| 1454 | + return | |
| 1455 | + } | |
| 1456 | + back := "/~" + org.Name + "/settings" | |
| 1457 | + if r.Method == "POST" && action == "members" { | |
| 1458 | + name := strings.TrimPrefix(strings.TrimSpace(r.FormValue("name")), "~") | |
| 1459 | + target, err := s.store.UserByName(name) | |
| 1460 | + switch { | |
| 1461 | + case err != nil || s.store.IsOrg(target.ID): | |
| 1462 | + back += "?error=No+such+user" | |
| 1463 | + case r.FormValue("remove") != "": | |
| 1464 | + if len(s.store.Members(org.ID)) == 1 { | |
| 1465 | + back += "?error=An+organisation+needs+at+least+one+member" | |
| 1466 | + } else { | |
| 1467 | + s.store.SetMember(org.ID, target.ID, false) | |
| 1468 | + } | |
| 1469 | + default: | |
| 1470 | + s.store.SetMember(org.ID, target.ID, true) | |
| 1471 | + } | |
| 1472 | + http.Redirect(w, r, back, http.StatusSeeOther) | |
| 1473 | + return | |
| 1474 | + } | |
| 1475 | + if r.Method != "GET" || action != "settings" { | |
| 1476 | + http.NotFound(w, r) | |
| 1477 | + return | |
| 1478 | + } | |
| 1479 | + var people []userHit | |
| 1480 | + for _, m := range s.store.Members(org.ID) { | |
| 1481 | + people = append(people, userHit{User: m, Profile: s.store.Profile(m.ID)}) | |
| 1482 | + } | |
| 1483 | + s.render(w, r, "orgsettings", page{Title: "~" + org.Name + " settings", User: u, Error: r.URL.Query().Get("error"), | |
| 1484 | + Data: map[string]any{"Org": org, "Profile": s.store.Profile(org.ID), "Members": people}}) | |
| 1485 | +} | |