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 3 years 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,
	}
}