dmitri.shuralyov.com/website/gido/...

factor out common components from issues/changes pages

This makes the page rendering code in serveIssues and serveChanges
methods more consistently high-level, and as a result, more readable.

It will make it easier to add new similar issues/changes pages that
display entries for import path patterns.
dmitshur committed 6 months ago commit 2378d509cdc3e6c1b27098cbc028973cdd3c186c
changes.go
@@ -6,17 +6,13 @@ import (
 	"net/http"
 	"net/url"
 	"os"
 	"sort"
 
-	"dmitri.shuralyov.com/app/changes/component"
 	"dmitri.shuralyov.com/service/change"
 	"github.com/shurcooL/htmlg"
 	"github.com/shurcooL/httperror"
-	"github.com/shurcooL/octicon"
-	"golang.org/x/net/html"
-	"golang.org/x/net/html/atom"
 )
 
 var changesHTML = template.Must(template.New("").Parse(`{{define "Header"}}<html lang="en">
 	<head>
 {{.AnalyticsHTML}}		<title>{{with .PageName}}{{.}} - {{end}}Go Changes</title>
@@ -91,64 +87,25 @@ func (h *handler) serveChanges(w http.ResponseWriter, req *http.Request, pkg str
 		cs = ic.ClosedChanges
 	case filter == change.FilterAll:
 		cs = append(ic.OpenChanges, ic.ClosedChanges...) // TODO: Measure if slow, optimize if needed.
 		sort.Slice(cs, func(i, j int) bool { return cs[i].ID > cs[j].ID })
 	}
-	openCount := uint64(len(ic.OpenChanges))
-	closedCount := uint64(len(ic.ClosedChanges))
 
 	w.Header().Set("Content-Type", "text/html; charset=utf-8")
 	err = h.executeTemplate(w, req, "Header", map[string]interface{}{
 		"PageName":      pkg,
 		"AnalyticsHTML": h.analyticsHTML,
 	})
 	if err != nil {
 		return err
 	}
-	heading := htmlg.NodeComponent{
-		Type: html.ElementNode, Data: atom.H2.String(),
-		Attr:       []html.Attribute{{Key: atom.Style.String(), Val: "margin-top: 30px;"}},
-		FirstChild: htmlg.Text(pkg),
-	}
-	if pkg == otherPackages {
-		heading.Data, heading.FirstChild = atom.H3.String(), htmlg.Text("Other Go Issues/Changes")
-	}
-	tabnav := tabnav{
-		Tabs: []tab{
-			{
-				Content: contentCounter{
-					Content: iconText{Icon: octicon.IssueOpened, Text: "Issues"},
-					Count:   len(ic.OpenIssues),
-				},
-				URL: h.rtr.IssuesURL(pkg),
-			},
-			{
-				Content: contentCounter{
-					Content: iconText{Icon: octicon.GitPullRequest, Text: "Changes"},
-					Count:   len(ic.OpenChanges),
-				},
-				URL:      h.rtr.ChangesURL(pkg),
-				Selected: true,
-			},
-		},
-	}
-	var es []component.ChangeEntry
-	for _, c := range cs {
-		es = append(es, component.ChangeEntry{Change: c, BaseURI: "https://golang.org/cl"})
-	}
-	changes := component.Changes{
-		ChangesNav: component.ChangesNav{
-			OpenCount:     openCount,
-			ClosedCount:   closedCount,
-			Path:          req.URL.Path,
-			Query:         req.URL.Query(),
-			StateQueryKey: stateQueryKey,
-		},
-		Filter:  filter,
-		Entries: es,
-	}
-	err = htmlg.RenderComponents(w, heading, subheading{pkg}, tabnav, changes)
+	err = htmlg.RenderComponents(w,
+		heading{Pkg: pkg},
+		subheading{Pkg: pkg},
+		renderTabnav(changesTab, len(ic.OpenIssues), len(ic.OpenChanges), pkg, h.rtr),
+		renderChanges(cs, len(ic.OpenChanges), len(ic.ClosedChanges), req.URL, filter),
+	)
 	if err != nil {
 		return err
 	}
 	err = h.executeTemplate(w, req, "Trailer", nil)
 	return err
issues.go
@@ -9,14 +9,10 @@ import (
 	"sort"
 
 	"github.com/shurcooL/htmlg"
 	"github.com/shurcooL/httperror"
 	"github.com/shurcooL/issues"
-	"github.com/shurcooL/issuesapp/component"
-	"github.com/shurcooL/octicon"
-	"golang.org/x/net/html"
-	"golang.org/x/net/html/atom"
 )
 
 var issuesHTML = template.Must(template.New("").Parse(`{{define "Header"}}<html lang="en">
 	<head>
 {{.AnalyticsHTML}}		<title>{{with .PageName}}{{.}} - {{end}}Go Issues</title>
@@ -91,88 +87,33 @@ func (h *handler) serveIssues(w http.ResponseWriter, req *http.Request, pkg stri
 		is = ic.ClosedIssues
 	case filter == issues.AllStates:
 		is = append(ic.OpenIssues, ic.ClosedIssues...) // TODO: Measure if slow, optimize if needed.
 		sort.Slice(is, func(i, j int) bool { return is[i].ID > is[j].ID })
 	}
-	openCount := uint64(len(ic.OpenIssues))
-	closedCount := uint64(len(ic.ClosedIssues))
 
 	w.Header().Set("Content-Type", "text/html; charset=utf-8")
 	err = h.executeTemplate(w, req, "Header", map[string]interface{}{
 		"PageName":      pkg,
 		"AnalyticsHTML": h.analyticsHTML,
 	})
 	if err != nil {
 		return err
 	}
-	heading := htmlg.NodeComponent{
-		Type: html.ElementNode, Data: atom.H2.String(),
-		Attr:       []html.Attribute{{Key: atom.Style.String(), Val: "margin-top: 30px;"}},
-		FirstChild: htmlg.Text(pkg),
-	}
-	if pkg == otherPackages {
-		heading.Data, heading.FirstChild = atom.H3.String(), htmlg.Text("Other Go Issues/Changes")
-	}
-	tabnav := tabnav{
-		Tabs: []tab{
-			{
-				Content: contentCounter{
-					Content: iconText{Icon: octicon.IssueOpened, Text: "Issues"},
-					Count:   len(ic.OpenIssues),
-				},
-				URL:      h.rtr.IssuesURL(pkg),
-				Selected: true,
-			},
-			{
-				Content: contentCounter{
-					Content: iconText{Icon: octicon.GitPullRequest, Text: "Changes"},
-					Count:   len(ic.OpenChanges),
-				},
-				URL: h.rtr.ChangesURL(pkg),
-			},
-		},
-	}
-	title := ImportPathToFullPrefix(pkg)
-	newIssue := htmlg.NodeComponent{
-		Type: html.ElementNode, Data: atom.Div.String(),
-		Attr:       []html.Attribute{{Key: atom.Style.String(), Val: "text-align: right;"}},
-		FirstChild: htmlg.A("New Issue", "https://golang.org/issue/new?title="+url.QueryEscape(title)),
-	}
-	var es []component.IssueEntry
-	for _, i := range is {
-		es = append(es, component.IssueEntry{Issue: i, BaseURI: "https://golang.org/issue"})
-	}
-	issues := component.Issues{
-		IssuesNav: component.IssuesNav{
-			OpenCount:     openCount,
-			ClosedCount:   closedCount,
-			Path:          req.URL.Path,
-			Query:         req.URL.Query(),
-			StateQueryKey: stateQueryKey,
-		},
-		Filter:  filter,
-		Entries: es,
-	}
-	err = htmlg.RenderComponents(w, heading, subheading{pkg}, tabnav, newIssue, issues)
+	err = htmlg.RenderComponents(w,
+		heading{Pkg: pkg},
+		subheading{Pkg: pkg},
+		renderTabnav(issuesTab, len(ic.OpenIssues), len(ic.OpenChanges), pkg, h.rtr),
+		renderNewIssue(pkg),
+		renderIssues(is, len(ic.OpenIssues), len(ic.ClosedIssues), req.URL, filter),
+	)
 	if err != nil {
 		return err
 	}
 	err = h.executeTemplate(w, req, "Trailer", nil)
 	return err
 }
 
-type subheading struct{ Pkg string }
-
-func (s subheading) Render() []*html.Node {
-	switch s.Pkg {
-	case otherPackages:
-		return []*html.Node{htmlg.P(htmlg.Text("Issues and changes that don't fit into any existing Go package."))}
-	default:
-		return nil
-	}
-}
-
 const (
 	// stateQueryKey is name of query key for controlling issue/change state filter.
 	stateQueryKey = "state"
 )
 
page.go
@@ -0,0 +1,122 @@
+package main
+
+import (
+	"net/url"
+
+	changescomponent "dmitri.shuralyov.com/app/changes/component"
+	"dmitri.shuralyov.com/service/change"
+	"github.com/shurcooL/htmlg"
+	"github.com/shurcooL/issues"
+	issuescomponent "github.com/shurcooL/issuesapp/component"
+	"github.com/shurcooL/octicon"
+	"golang.org/x/net/html"
+	"golang.org/x/net/html/atom"
+)
+
+type heading struct{ Pkg string }
+
+func (h heading) Render() []*html.Node {
+	switch h.Pkg {
+	default:
+		h2 := &html.Node{
+			Type: html.ElementNode, Data: atom.H2.String(),
+			Attr:       []html.Attribute{{Key: atom.Style.String(), Val: "margin-top: 30px;"}},
+			FirstChild: htmlg.Text(h.Pkg),
+		}
+		return []*html.Node{h2}
+	case otherPackages:
+		h3 := &html.Node{
+			Type: html.ElementNode, Data: atom.H3.String(),
+			Attr:       []html.Attribute{{Key: atom.Style.String(), Val: "margin-top: 30px;"}},
+			FirstChild: htmlg.Text("Other Go Issues/Changes"),
+		}
+		return []*html.Node{h3}
+	}
+}
+
+type subheading struct{ Pkg string }
+
+func (s subheading) Render() []*html.Node {
+	switch s.Pkg {
+	case otherPackages:
+		return []*html.Node{htmlg.P(htmlg.Text("Issues and changes that don't fit into any existing Go package."))}
+	default:
+		return nil
+	}
+}
+
+func renderTabnav(selected pageTab, openIssues, openChanges int, pattern string, rtr Router) htmlg.Component {
+	return tabnav{
+		Tabs: []tab{
+			{
+				Content: contentCounter{
+					Content: iconText{Icon: octicon.IssueOpened, Text: "Issues"},
+					Count:   openIssues,
+				},
+				URL:      rtr.IssuesURL(pattern),
+				Selected: selected == issuesTab,
+			},
+			{
+				Content: contentCounter{
+					Content: iconText{Icon: octicon.GitPullRequest, Text: "Changes"},
+					Count:   openChanges,
+				},
+				URL:      rtr.ChangesURL(pattern),
+				Selected: selected == changesTab,
+			},
+		},
+	}
+}
+
+type pageTab uint8
+
+const (
+	_ pageTab = iota
+	issuesTab
+	changesTab
+)
+
+func renderNewIssue(pkg string) htmlg.Component {
+	title := ImportPathToFullPrefix(pkg)
+	return htmlg.NodeComponent{
+		Type: html.ElementNode, Data: atom.Div.String(),
+		Attr:       []html.Attribute{{Key: atom.Style.String(), Val: "text-align: right;"}},
+		FirstChild: htmlg.A("New Issue", "https://golang.org/issue/new?title="+url.QueryEscape(title)),
+	}
+}
+
+func renderIssues(is []issues.Issue, openIssues, closedIssues int, reqURL *url.URL, filter issues.StateFilter) htmlg.Component {
+	var es []issuescomponent.IssueEntry
+	for _, i := range is {
+		es = append(es, issuescomponent.IssueEntry{Issue: i, BaseURI: "https://golang.org/issue"})
+	}
+	return issuescomponent.Issues{
+		IssuesNav: issuescomponent.IssuesNav{
+			OpenCount:     uint64(openIssues),
+			ClosedCount:   uint64(closedIssues),
+			Path:          reqURL.Path,
+			Query:         reqURL.Query(),
+			StateQueryKey: stateQueryKey,
+		},
+		Filter:  filter,
+		Entries: es,
+	}
+}
+
+func renderChanges(cs []change.Change, openChanges, closedChanges int, reqURL *url.URL, filter change.StateFilter) htmlg.Component {
+	var es []changescomponent.ChangeEntry
+	for _, c := range cs {
+		es = append(es, changescomponent.ChangeEntry{Change: c, BaseURI: "https://golang.org/cl"})
+	}
+	return changescomponent.Changes{
+		ChangesNav: changescomponent.ChangesNav{
+			OpenCount:     uint64(openChanges),
+			ClosedCount:   uint64(closedChanges),
+			Path:          reqURL.Path,
+			Query:         reqURL.Query(),
+			StateQueryKey: stateQueryKey,
+		},
+		Filter:  filter,
+		Entries: es,
+	}
+}