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

Add /-/packages page that lists all packages.

Sometimes it's helpful to be able to see a list of all packages (and
their number of open issues) at a glance.

Since they're all on one page, it's easy to use the browser's search
functionality to find a specific package or parent folder.

If Accept header is set to exactly "application/json", respond in JSON
format.
dmitshur committed 1 year ago commit dcee4de78e6ca89e867023847cd62ee6913b57a7
main.go
@@ -1,10 +1,11 @@
 // gido is the command that powers the https://goissues.org website.
 package main
 
 import (
 	"context"
+	"encoding/json"
 	"flag"
 	"fmt"
 	"io"
 	"io/ioutil"
 	"log"
@@ -116,10 +117,15 @@ func (h *handler) ServeHTTP(w http.ResponseWriter, req *http.Request) error {
 	if req.URL.Path == "/assets" || strings.HasPrefix(req.URL.Path, "/assets/") {
 		h.assetsHandler.ServeHTTP(w, req)
 		return nil
 	}
 
+	// Handle "/-/packages".
+	if req.URL.Path == "/-/packages" {
+		return h.ServePackages(w, req)
+	}
+
 	// Handle "/..." URLs.
 	if canonicalPath := path.Clean(req.URL.Path); req.URL.Path != canonicalPath {
 		// Redirect to canonical path (no trailing slash, etc.).
 		if req.URL.RawQuery != "" {
 			canonicalPath += "?" + req.URL.RawQuery
@@ -139,11 +145,12 @@ const htmlPart1, htmlPart2, htmlPart3 = `<html>
 		<link href="/assets/style.css" rel="stylesheet" type="text/css">
 	</head>
 	<body style="margin: 0; position: relative;">
 		<header style="background-color: hsl(209, 51%, 92%);">
 			<div style="max-width: 800px; margin: 0 auto 0 auto; padding: 0 15px 0 15px;">
-				<a class="black" href="/"><strong style="padding: 15px 0 15px 0; display: inline-block;">Go Issues</strong></a>
+				<a class="black" href="/"                                      ><strong style="padding: 15px 0 15px 0; display: inline-block;">Go Issues</strong></a>
+				<a class="black" href="/-/packages" style="padding-left: 30px;"><span   style="padding: 15px 0 15px 0; display: inline-block;">Packages</span></a>
 			</div>
 		</header>
 
 		<main style="max-width: 800px; margin: 0 auto 0 auto; padding: 0 15px 120px 15px;">`, `</main>
 
@@ -154,10 +161,11 @@ const htmlPart1, htmlPart2, htmlPart3 = `<html>
 		</footer>
 	</body>
 </html>
 `
 
+// ServeIndex serves the index page.
 func (h *handler) ServeIndex(w http.ResponseWriter, req *http.Request) error {
 	if req.Method != http.MethodGet {
 		return httperror.Method{Allowed: []string{http.MethodGet}}
 	}
 
@@ -185,15 +193,15 @@ func (h *handler) ServeIndex(w http.ResponseWriter, req *http.Request) error {
 			using your browser's address bar (<kbd>⌘Cmd</kbd>+<kbd>L</kbd> or <kbd>Ctrl</kbd>+<kbd>L</kbd>).</p>
 
 			<p>Supported import paths include:</p>
 
 			<ul>
-			<li><a href="https://golang.org/pkg/#stdlib">Standard library</a> (e.g., <code>io</code>, <code>net/http</code>, etc.),</li>
-			<li><a href="https://golang.org/pkg/#subrepo">Sub-repositories</a> (i.e., <code>golang.org/x/...</code>).</li>
+			<li><a href="/-/packages#stdlib">Standard library</a> (e.g., <code>io</code>, <code>net/http</code>, etc.),</li>
+			<li><a href="/-/packages#subrepo">Sub-repositories</a> (i.e., <code>golang.org/x/...</code>).</li>
 			</ul>
 
-			<p>Import paths of 3rd party packages (e.g., <code>github.com/...</code>) are not supported.</p>
+			<p>Import paths of 3rd party packages (e.g., <code>github.com/...</code>) are not supported at this time.</p>
 
 			<p>It's a simple website with a narrow scope. Enjoy. ʕ◔ϖ◔ʔ</p>
 		`)
 	if err != nil {
 		return err
@@ -226,13 +234,97 @@ func (h *handler) ServeIndex(w http.ResponseWriter, req *http.Request) error {
 
 	_, err = io.WriteString(w, htmlPart3)
 	return err
 }
 
+// ServePackages serves a list of all known packages.
+func (h *handler) ServePackages(w http.ResponseWriter, req *http.Request) error {
+	if req.Method != http.MethodGet {
+		return httperror.Method{Allowed: []string{http.MethodGet}}
+	}
+
+	// Gather all packages in sorted order.
+	h.s.PackageIssuesMu.RLock()
+	pis := h.s.PackageIssues
+	h.s.PackageIssuesMu.RUnlock()
+	var stdlib, subrepo []pkg
+	for _, p := range h.s.Packages {
+		switch isStandard(p) {
+		case true:
+			stdlib = append(stdlib, pkg{
+				Path: p,
+				Open: len(pis[p].Open),
+			})
+		case false:
+			subrepo = append(subrepo, pkg{
+				Path: p,
+				Open: len(pis[p].Open),
+			})
+		}
+	}
+
+	if req.Header.Get("Accept") == "application/json" {
+		w.Header().Set("Content-Type", "application/json")
+		e := json.NewEncoder(w)
+		e.SetIndent("", "\t")
+		err := e.Encode(append(stdlib, subrepo...)) // TODO: Measure if slow, optimize if needed.
+		return err
+	}
+
+	w.Header().Set("Content-Type", "text/html; charset=utf-8")
+	_, err := io.WriteString(w, htmlPart1)
+	if err != nil {
+		return err
+	}
+	_, err = w.Write(h.analyticsHTML)
+	if err != nil {
+		return err
+	}
+	_, err = io.WriteString(w, htmlPart2)
+	if err != nil {
+		return err
+	}
+
+	// Render the stdlib section.
+	_, err = io.WriteString(w, `<h3 id="stdlib" style="margin-top: 30px;">Standard Library Packages</h3>`)
+	if err != nil {
+		return err
+	}
+	err = renderTable(w, stdlib)
+	if err != nil {
+		return err
+	}
+
+	// Render the subrepo section.
+	_, err = io.WriteString(w, `<h3 id="subrepo" style="margin-top: 30px;">Sub-repository Packages</h3>`)
+	if err != nil {
+		return err
+	}
+	err = renderTable(w, subrepo)
+	if err != nil {
+		return err
+	}
+
+	// Write note about other issues.
+	_, err = io.WriteString(w, `<h3 id="other" style="margin-top: 30px;">Other Issues</h3>
+
+		<p>
+			Issues from the <a href="https://golang.org/issues">Go issue tracker</a>
+			that don't fit into any existing Go package
+			can be seen under <a href="/other">other</a>.
+		</p>`)
+	if err != nil {
+		return err
+	}
+
+	_, err = io.WriteString(w, htmlPart3)
+	return err
+}
+
 type pkg struct {
-	Path string
-	Open int
+	Path string `json:"ImportPath"`
+	Open int    `json:"OpenIssues"`
 }
 
 func renderTable(w io.Writer, pkgs []pkg) error {
 	_, err := io.WriteString(w, `<table class="table table-sm">
 		<thead>
@@ -300,14 +392,16 @@ func (h *handler) ServeIssues(w http.ResponseWriter, req *http.Request, pkg stri
 	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),
 	}
 	title := pkg + ": "
 	if pkg == otherPackages {
+		heading.Data, heading.FirstChild = atom.H3.String(), htmlg.Text("Other Go Issues")
 		title = ""
 	}
 	newIssue := htmlg.NodeComponent{
 		Type: html.ElementNode, Data: atom.Div.String(),
 		Attr:       []html.Attribute{{Key: atom.Style.String(), Val: "text-align: right;"}},