Template the frontend

Amolith created

Change summary

.air.toml                                  |  2 
ws/static/dashboard.html.tmpl              | 59 ++++++++++++++
ws/static/dashboard.html.tmpl.license      |  0 
ws/static/footer.html.tmpl                 |  9 ++
ws/static/footer.html.tmpl.license         |  0 
ws/static/head.html.tmpl                   | 19 ----
ws/static/head.html.tmpl.license           |  0 
ws/static/header.html.tmpl                 | 11 ++
ws/static/header.html.tmpl.license         |  0 
ws/static/home.html                        | 96 ------------------------
ws/static/login.html.tmpl                  | 20 +++++
ws/static/login.html.tmpl.license          |  3 
ws/static/new.html.tmpl                    | 31 +------
ws/static/new.html.tmpl.license            |  3 
ws/static/select-release.html.tmpl         | 32 +------
ws/static/select-release.html.tmpl.license |  3 
ws/static/styles.css                       | 18 ++--
ws/ws.go                                   | 38 +++++---
18 files changed, 157 insertions(+), 187 deletions(-)

Detailed changes

.air.toml 🔗

@@ -7,7 +7,7 @@ tmp_dir = "tmp"
 
 [build]
   bin = "./tmp/willow"
-  cmd = "go build -o ./tmp/willow ./cmd"
+  cmd = "go build -o ./tmp/willow -ldflags \"-X main.version=`git describe --long 2>/dev/null | sed 's/\\([^-]*-g\\)/r\\1/;s/-/./g'`\" ./cmd"
   delay = 1000
   exclude_dir = ["assets", "tmp", "vendor", "testdata", "data", "website"]
   exclude_file = []

ws/static/dashboard.html.tmpl 🔗

@@ -0,0 +1,59 @@
+{{- template "head" }}
+{{- template "header" }}
+        <main>
+            <div class="wrapper two_column">
+                <div class="projects">
+                    <!-- Range through projects that aren't yet up-to-date -->
+                    {{- range .Projects -}}
+                    {{- if ne .Running (index .Releases 0).Tag -}}
+                    <h2>Outdated projects</h2>
+                    {{- break -}}
+                    {{- end -}}
+                    {{- end -}}
+                    {{- range .Projects -}}
+                    {{- if ne .Running (index .Releases 0).Tag -}}
+                    <div id="{{ .ID }}" class="project card">
+                        <h3><a href="{{ .URL }}">{{ .Name }}</a>&nbsp;&nbsp;&nbsp;<span class="delete"><a href="/new?action=delete&id={{ .ID }}">Delete?</a></span></h3>
+                        <p>You've selected {{ .Running }}. <a href="/new?action=update&url={{ .URL }}&forge={{ .Forge }}&name={{ .Name }}">Modify?</a></p>
+                        <p>Latest: <a href="{{ (index .Releases 0).URL }}">{{ (index .Releases 0).Tag }}</a></p>
+                        <p><a href="#{{ (index .Releases 0).ID }}">View release notes</a></p>
+                    </div>
+                    {{- end -}}
+                    {{- end -}}
+
+                    <!-- Range through projects that _are_ up-to-date -->
+                    {{- range .Projects -}}
+                    {{- if eq .Running (index .Releases 0).Tag -}}
+                    <h2>Up-to-date projects</h2>
+                    {{- break -}}
+                    {{- end -}}
+                    {{- end -}}
+                    {{- range .Projects -}}
+                    {{- if eq .Running (index .Releases 0).Tag -}}
+                    <div class="project card">
+                        <h3><a href="{{ .URL }}">{{ .Name }}</a>&nbsp;&nbsp;&nbsp;<span class="delete"><a href="/new?action=delete&id={{ .ID }}">Delete?</a></span></h3>
+                        <p>You've selected <a href="#{{ (index .Releases 0).ID }}">{{ .Running }}</a>. <a href="/new?action=update&url={{ .URL }}&forge={{ .Forge }}&name={{ .Name }}">Modify?</a></p>
+                    </div>
+                    {{- end -}}
+                    {{- end -}}
+                </div>
+                <div class="release_notes">
+                    <h2>Release notes</h2>
+                    {{- range .Projects -}}
+                    <div id="{{ (index .Releases 0).ID }}" class="release_note card">
+                        <h3>{{ .Name }}: release notes for <a href="{{ (index .Releases 0).URL }}">{{ (index .Releases 0).Tag }}</a></h3>
+                        {{- if eq .Forge "github" "gitea" "forgejo" -}}
+                        {{- (index .Releases 0).Content -}}
+                        {{- else -}}
+                        <pre>
+                        {{- (index .Releases 0).Content -}}
+                        </pre>
+                        {{- end -}}
+                        <p><a class="return_to_project" href="#{{ .ID }}">Back to project</a></p>
+                        <div class="close"><a href="#">&#x2716;</a></div>
+                    </div>
+                    {{- end -}}
+                </div>
+            </div>
+        </main>
+{{- template "footer" .Version }}

ws/static/footer.html.tmpl 🔗

@@ -0,0 +1,9 @@
+{{- define "footer" }}
+        <footer>
+            <div class="wrapper">
+                <p>Willow {{ . }} &mdash; <a href="https://getwillow.org">Source code</a> &mdash; <a href="https://todo.sr.ht/~amolith/willow">Issue queue</a></p>
+            </div>
+        </footer>
+    </body>
+</html>
+{{- end -}}

ws/static/login.html → ws/static/head.html.tmpl 🔗

@@ -1,3 +1,4 @@
+{{- define "head" }}
 <!DOCTYPE html>
 <html lang="en-GB">
     <head>
@@ -19,19 +20,5 @@
         <link rel="preload" href="/static/styles.css" as="style" />
         <link rel="stylesheet" href="/static/styles.css" />
     </head>
-    <body class="old-wrapper">
-        <h1>Willow</h1>
-        <form method="post">
-            <div class="input">
-                <label for="username">Username:</label>
-                <input type="text" id="username" name="username">
-            </div>
-            <div class="input">
-                <label for="password">Password:</label>
-                <input type="password" id="password" name="password">
-            </div>
-            <input class="button" type="submit" formaction="/login" value="Login">
-        </form>
-        <p><a href="https://sr.ht/~amolith/willow">Source code</a></p>
-    </body>
-</html>
+    <body>
+{{- end -}}

ws/static/header.html.tmpl 🔗

@@ -0,0 +1,11 @@
+{{- define "header" }}
+        <header>
+            <div class="wrapper">
+                <h1>Willow</h1>
+                <nav>
+                    <a href="/new">Track a new project</a>
+                    <a href="/logout">Log out</a>
+                </nav>
+            </div>
+        </header>
+{{- end -}}

ws/static/home.html 🔗

@@ -1,96 +0,0 @@
-<!DOCTYPE html>
-<html lang="en-GB">
-    <head>
-        <title>Willow</title>
-        <meta name="viewport" content="width=device-width, initial-scale=1.0">
-        <meta name="title" content="Willow">
-        <meta name="description" content="Forge-agnostic software release tracker">
-
-        <!-- Indicate that we support both light and dark mode -->
-        <meta name="color-scheme" content="dark light">
-
-        <!-- Preload CSS reset -->
-        <link rel="preload" href="/static/reset.css" as="style" />
-        <link rel="stylesheet" href="/static/reset.css" />
-
-        <!-- Preload CSS styles -->
-        <link rel="preload" href="/static/colours.css" as="style" />
-        <link rel="stylesheet" href="/static/colours.css" />
-        <link rel="preload" href="/static/styles.css" as="style" />
-        <link rel="stylesheet" href="/static/styles.css" />
-    </head>
-    <body>
-        <div class="container">
-        <header>
-            <div class="wrapper">
-                <h1>Willow</h1>
-                <nav>
-                    <a href="/new">Track a new project</a>
-                    <a href="/logout">Log out</a>
-                </nav>
-            </div>
-        </header>
-        <main>
-            <div class="wrapper two_column">
-            <div class="projects">
-                <!-- Range through projects that aren't yet up-to-date -->
-                {{- range .Projects -}}
-                {{- if ne .Running (index .Releases 0).Tag -}}
-                <h2>Outdated projects</h2>
-                {{- break -}}
-                {{- end -}}
-                {{- end -}}
-                {{- range .Projects -}}
-                {{- if ne .Running (index .Releases 0).Tag -}}
-                <div id="{{ .ID }}" class="project card">
-                    <h3><a href="{{ .URL }}">{{ .Name }}</a>&nbsp;&nbsp;&nbsp;<span class="delete"><a href="/new?action=delete&id={{ .ID }}">Delete?</a></span></h3>
-                    <p>You've selected {{ .Running }}. <a href="/new?action=update&url={{ .URL }}&forge={{ .Forge }}&name={{ .Name }}">Modify?</a></p>
-                    <p>Latest: <a href="{{ (index .Releases 0).URL }}">{{ (index .Releases 0).Tag }}</a></p>
-                    <p><a href="#{{ (index .Releases 0).ID }}">View release notes</a></p>
-                </div>
-                {{- end -}}
-                {{- end -}}
-
-                <!-- Range through projects that _are_ up-to-date -->
-                {{- range .Projects -}}
-                {{- if eq .Running (index .Releases 0).Tag -}}
-                <h2>Up-to-date projects</h2>
-                {{- break -}}
-                {{- end -}}
-                {{- end -}}
-                {{- range .Projects -}}
-                {{- if eq .Running (index .Releases 0).Tag -}}
-                <div class="project card">
-                    <h3><a href="{{ .URL }}">{{ .Name }}</a>&nbsp;&nbsp;&nbsp;<span class="delete"><a href="/new?action=delete&id={{ .ID }}">Delete?</a></span></h3>
-                    <p>You've selected <a href="#{{ (index .Releases 0).ID }}">{{ .Running }}</a>. <a href="/new?action=update&url={{ .URL }}&forge={{ .Forge }}&name={{ .Name }}">Modify?</a></p>
-                </div>
-                {{- end -}}
-                {{- end -}}
-            </div>
-            <div class="release_notes">
-                <h2>Release notes</h2>
-                {{- range .Projects -}}
-                <div id="{{ (index .Releases 0).ID }}" class="release_note card">
-                    <h3>{{ .Name }}: release notes for <a href="{{ (index .Releases 0).URL }}">{{ (index .Releases 0).Tag }}</a></h3>
-                    {{- if eq .Forge "github" "gitea" "forgejo" -}}
-                    {{- (index .Releases 0).Content -}}
-                    {{- else -}}
-                    <pre>
-                    {{- (index .Releases 0).Content -}}
-                    </pre>
-                    {{- end -}}
-                    <p><a class="return_to_project" href="#{{ .ID }}">Back to project</a></p>
-                    <div class="close"><a href="#">&#x2716;</a></div>
-                </div>
-                {{- end -}}
-            </div>
-            </div>
-        </main>
-        <footer>
-            <div class="wrapper">
-              <p>Willow {{ .Version }} &mdash; <a href="https://getwillow.org">Source code</a> &mdash; <a href="https://todo.sr.ht/~amolith/willow">Issue queue</a></p>
-            </div>
-        </footer>
-        </div>
-    </body>
-</html>

ws/static/login.html.tmpl 🔗

@@ -0,0 +1,20 @@
+{{- template "head" -}}
+        <header>
+            <div class="wrapper">
+                <h1>Willow</h1>
+            </div>
+        </header>
+        <div class="old-wrapper">
+            <form method="post">
+                <div class="input">
+                    <label for="username">Username:</label>
+                    <input type="text" id="username" name="username">
+                </div>
+                <div class="input">
+                    <label for="password">Password:</label>
+                    <input type="password" id="password" name="password">
+                </div>
+                <input class="button" type="submit" formaction="/login" value="Login">
+            </form>
+        </div>
+{{- template "footer" .Version -}}

ws/static/new.html → ws/static/new.html.tmpl 🔗

@@ -1,26 +1,7 @@
-<!DOCTYPE html>
-<html lang="en-GB">
-    <head>
-        <title>Willow</title>
-        <meta name="viewport" content="width=device-width, initial-scale=1.0">
-        <meta name="title" content="Willow">
-        <meta name="description" content="Forge-agnostic software release tracker">
-
-        <!-- Indicate that we support both light and dark mode -->
-        <meta name="color-scheme" content="dark light">
-
-        <!-- Preload CSS reset -->
-        <link rel="preload" href="/static/reset.css" as="style" />
-        <link rel="stylesheet" href="/static/reset.css" />
-
-        <!-- Preload CSS styles -->
-        <link rel="preload" href="/static/colours.css" as="style" />
-        <link rel="stylesheet" href="/static/colours.css" />
-        <link rel="preload" href="/static/styles.css" as="style" />
-        <link rel="stylesheet" href="/static/styles.css" />
-    </head>
-    <body class="old-wrapper">
-        <h1>Willow</h1>
+{{- template "head" }}
+{{- template "header" }}
+        <div class="old-wrapper">
+        <h2>Track a new project</h2>
         <form method="post">
             <div class="input">
                 <label for="url">Project URL:</label>
@@ -51,5 +32,5 @@
             </div>
             <input class="button" type="submit" formaction="/new" value="Next">
         </form>
-    </body>
-</html>
+        </div>
+{{- template "footer" .Version }}

ws/static/select-release.html → ws/static/select-release.html.tmpl 🔗

@@ -1,26 +1,7 @@
-<!DOCTYPE html>
-<html lang="en-GB">
-    <head>
-        <title>Willow</title>
-        <meta name="viewport" content="width=device-width, initial-scale=1.0">
-        <meta name="title" content="Willow">
-        <meta name="description" content="Forge-agnostic software release tracker">
-
-        <!-- Indicate that we support both light and dark mode -->
-        <meta name="color-scheme" content="dark light">
-
-        <!-- Preload CSS reset -->
-        <link rel="preload" href="/static/reset.css" as="style" />
-        <link rel="stylesheet" href="/static/reset.css" />
-
-        <!-- Preload CSS styles -->
-        <link rel="preload" href="/static/colours.css" as="style" />
-        <link rel="stylesheet" href="/static/colours.css" />
-        <link rel="preload" href="/static/styles.css" as="style" />
-        <link rel="stylesheet" href="/static/styles.css" />
-    </head>
-    <body class="old-wrapper">
-        <h1>Willow</h1>
+{{- template "head" }}
+{{- template "header" }}
+{{- with .Project }}
+        <div class="old-wrapper">
         <form method="post">
             <div class="input">
                 <p>Which release of {{ .Name }} are you currently running?</p>
@@ -52,5 +33,6 @@
         {{- if or (eq $forge "github") -}}
         <small>Some RSS feeds (notably GitHub's) include a limited number of releases. If you don't see your version, please change the forge type to "Other".</small>
         {{- end -}}
-    </body>
-</html>
+        </div>
+{{- end }}
+{{- template "footer" .Version -}}

ws/static/styles.css 🔗

@@ -53,34 +53,34 @@ a:visited {
 }
 
 /* Grid layout */
-.container {
+body {
 	width: auto;
 	min-height: 100vh;
 }
 
 @supports (display: grid) {
-	.container {
+	body {
 		display: grid;
 		grid-template-rows: [header] auto [main] 1fr [footer] auto;
 		}
 
-	.container > header,
-	.container > main,
-	.container > footer {
+	body > header,
+	body > main,
+	body > footer {
 		display: grid;
 		grid-template-columns:
 			[page-start] minmax(1em, 1fr) [content] minmax(240px, 100ch) [page-end] minmax(1em, 1fr);
 	}
 
-	.container > main {
+	body > main {
 		grid-template-rows: [top-gutter] 1em [content] 1fr [bottom-gutter] 1em;
 	}
 
-	.container > footer {
+	body > footer {
 		grid-template-rows: [top-gutter] 2em [content] 1fr [bottom-gutter] 0.5em;
 	}
 
-	.container > header {
+	body > header {
 		grid-template-rows: [top-gutter] 0.5em [content] 1fr [bottom-gutter] 0.5em;
 	}
 
@@ -164,7 +164,7 @@ a:visited {
 
 .old-wrapper { /* used on non-home pages */
 	max-width: 500px;
-	margin: auto auto;
+	margin: 0 auto;
 }
 
 header .wrapper {

ws/ws.go 🔗

@@ -52,17 +52,15 @@ func (h Handler) RootHandler(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	type stuff struct {
+	data := struct {
 		Version  string
 		Projects []project.Project
-	}
-
-	data := stuff{
+	}{
 		Version:  *h.Version,
 		Projects: projectsWithReleases,
 	}
 
-	tmpl := template.Must(template.ParseFS(fs, "static/home.html"))
+	tmpl := template.Must(template.ParseFS(fs, "static/dashboard.html.tmpl", "static/head.html.tmpl", "static/header.html.tmpl", "static/footer.html.tmpl"))
 	if err := tmpl.Execute(w, data); err != nil {
 		fmt.Println(err)
 	}
@@ -77,8 +75,9 @@ func (h Handler) NewHandler(w http.ResponseWriter, r *http.Request) {
 	action := bmStrict.Sanitize(params.Get("action"))
 	if r.Method == http.MethodGet {
 		if action == "" {
-			tmpl := template.Must(template.ParseFS(fs, "static/new.html"))
-			if err := tmpl.Execute(w, nil); err != nil {
+			data := struct{ Version string }{Version: *h.Version}
+			tmpl := template.Must(template.ParseFS(fs, "static/new.html.tmpl", "static/head.html.tmpl", "static/header.html.tmpl", "static/footer.html.tmpl"))
+			if err := tmpl.Execute(w, data); err != nil {
 				fmt.Println(err)
 			}
 		} else if action != "delete" {
@@ -139,8 +138,16 @@ func (h Handler) NewHandler(w http.ResponseWriter, r *http.Request) {
 				return
 			}
 
-			tmpl := template.Must(template.ParseFS(fs, "static/select-release.html"))
-			if err := tmpl.Execute(w, proj); err != nil {
+			data := struct {
+				Version string
+				Project project.Project
+			}{
+				Version: *h.Version,
+				Project: proj,
+			}
+
+			tmpl := template.Must(template.ParseFS(fs, "static/select-release.html.tmpl", "static/head.html.tmpl", "static/header.html.tmpl", "static/footer.html.tmpl"))
+			if err := tmpl.Execute(w, data); err != nil {
 				fmt.Println(err)
 			}
 		} else if action == "delete" {
@@ -198,12 +205,13 @@ func (h Handler) LoginHandler(w http.ResponseWriter, r *http.Request) {
 			return
 		}
 
-		login, err := fs.ReadFile("static/login.html")
-		if err != nil {
-			fmt.Println("Error reading login.html:", err)
+		data := struct {
+			Version string
+		}{
+			Version: *h.Version,
 		}
-
-		if _, err := io.WriteString(w, string(login)); err != nil {
+		tmpl := template.Must(template.ParseFS(fs, "static/login.html.tmpl", "static/head.html.tmpl", "static/footer.html.tmpl"))
+		if err := tmpl.Execute(w, data); err != nil {
 			fmt.Println(err)
 		}
 	}
@@ -318,7 +326,7 @@ func StaticHandler(writer http.ResponseWriter, request *http.Request) {
 	if err != nil {
 		fmt.Println(err)
 	}
-	if _, err = io.WriteString(writer, string(home)); err != nil {
+	if _, err = io.Writer.Write(writer, home); err != nil {
 		fmt.Println(err)
 	}
 }