From 0eb2ae488d7e7472217532ab1f3a6c4cd307605f Mon Sep 17 00:00:00 2001
From: Gregor Best <gbe@unobtanium.de>
Date: Sun, 11 Apr 2021 23:45:41 +0200
Subject: [PATCH] Add template for detailed view

---
 handler-details.go    | 50 ++++++++++++++++++++++
 handler-index.go      | 98 +++++++++++++++++++++++++++++++++++++++++++
 main.go               |  6 ++-
 templates/base.tpl    | 18 ++++++++
 templates/details.tpl |  9 ++++
 templates/index.tpl   |  2 +-
 vino.go               | 23 ++++++++++
 7 files changed, 203 insertions(+), 3 deletions(-)
 create mode 100644 handler-details.go
 create mode 100644 handler-index.go
 create mode 100644 templates/base.tpl
 create mode 100644 templates/details.tpl

diff --git a/handler-details.go b/handler-details.go
new file mode 100644
index 0000000..b2bc880
--- /dev/null
+++ b/handler-details.go
@@ -0,0 +1,50 @@
+package main
+
+import (
+	"errors"
+	"html/template"
+	"log"
+	"net/http"
+	"strconv"
+)
+
+var detailsTemplate = template.Must(template.ParseFS(templateFS, "templates/base.tpl", "templates/details.tpl"))
+
+func (h Handler) details(w http.ResponseWriter, r *http.Request) {
+	log.Println("handling", r.Method, r.URL, "from", r.RemoteAddr)
+
+	if r.Method != "GET" {
+		httpError(w, "bad method", errors.New(r.Method), http.StatusMethodNotAllowed)
+		return
+	}
+
+	args := r.URL.Query()
+	log.Println("args", args)
+
+	if args.Get("id") == "" {
+		httpError(w, "bad request", errors.New("empty id"), http.StatusBadRequest)
+		return
+	}
+
+	id, err := strconv.Atoi(args.Get("id"))
+	if err != nil {
+		httpError(w, "can't parse id", err, http.StatusBadRequest)
+		return
+	}
+
+	v, err := LoadVino(r.Context(), h.db, id, false)
+	if err != nil {
+		httpError(w, "can't load wine data", err, http.StatusInternalServerError)
+		return
+	}
+
+	data := struct {
+		Wine Vino
+	}{v}
+
+	err = detailsTemplate.ExecuteTemplate(w, "details.tpl", data)
+	if err != nil {
+		log.Println("can't execute details template:", err)
+		return
+	}
+}
diff --git a/handler-index.go b/handler-index.go
new file mode 100644
index 0000000..92b9a07
--- /dev/null
+++ b/handler-index.go
@@ -0,0 +1,98 @@
+package main
+
+import (
+	"errors"
+	"fmt"
+	"html/template"
+	"io/ioutil"
+	"log"
+	"net/http"
+	"strconv"
+)
+
+var indexTemplate = template.Must(template.ParseFS(templateFS, "templates/base.tpl", "templates/index.tpl"))
+
+func (h Handler) index(w http.ResponseWriter, r *http.Request) {
+	log.Println("handling", r.Method, r.URL, "from", r.RemoteAddr)
+
+	if r.Method == "GET" {
+		wines, err := ListWines(r.Context(), h.db)
+		if err != nil {
+			httpError(w, "can't list wines", err, http.StatusInternalServerError)
+			return
+		}
+
+		w.Header().Add("content-type", "text/html")
+
+		data := struct {
+			Wines []Vino
+		}{wines}
+
+		err = indexTemplate.ExecuteTemplate(w, "index.tpl", data)
+		if err != nil {
+			log.Println("can't execute index template:", err)
+		}
+
+		return
+	}
+
+	if r.Method != "POST" {
+		httpError(w, "invalid method", errors.New(r.Method), http.StatusMethodNotAllowed)
+		return
+	}
+
+	name := r.FormValue("name")
+	if name == "" {
+		httpError(w, "bad name", errors.New("empty or missing"), http.StatusBadRequest)
+		return
+	}
+
+	if len(name) > 80 {
+		httpError(w, "bad name", errors.New("name too long, max length is 80"), http.StatusBadRequest)
+		return
+	}
+
+	ratingVal := r.FormValue("rating")
+
+	var (
+		rating int
+		err    error
+	)
+
+	if ratingVal != "" {
+		rating, err = strconv.Atoi(ratingVal)
+		if err != nil {
+			httpError(w, "can't convert rating", err, http.StatusBadRequest)
+			return
+		}
+	}
+
+	picFile, picHdr, err := r.FormFile("picture")
+	if err != nil {
+		httpError(w, "can't open picture file", err, http.StatusInternalServerError)
+		return
+	}
+	defer picFile.Close()
+
+	log.Println("picture", picHdr.Size, picHdr.Header.Get("Content-Type"))
+
+	picData, err := ioutil.ReadAll(picFile)
+	if err != nil {
+		httpError(w, "can't read picture file", err, http.StatusInternalServerError)
+		return
+	}
+
+	vino := Vino{
+		Name:    name,
+		Rating:  rating,
+		Picture: picData,
+	}
+
+	err = vino.Store(r.Context(), h.db)
+	if err != nil {
+		httpError(w, fmt.Sprintf("can't store wine %q", vino), err, http.StatusInternalServerError)
+		return
+	}
+
+	http.Redirect(w, r, "/", http.StatusSeeOther) // TODO: Is this the correct status?
+}
diff --git a/main.go b/main.go
index 6c983c6..046a765 100644
--- a/main.go
+++ b/main.go
@@ -2,7 +2,6 @@ package main
 
 import (
 	"embed"
-	"html/template"
 	"log"
 	"net/http"
 
@@ -12,11 +11,14 @@ import (
 
 //go:embed templates/*.tpl
 var templateFS embed.FS
-var templates = template.Must(template.ParseFS(templateFS, "templates/base.tpl", "templates/index.tpl"))
 
 //go:embed static/*
 var staticFS embed.FS
 
+type Handler struct {
+	db *sqlx.DB
+}
+
 func httpError(w http.ResponseWriter, msg string, err error, status int) {
 	log.Println(msg+":", err)
 	http.Error(w, msg+": "+err.Error(), status)
diff --git a/templates/base.tpl b/templates/base.tpl
new file mode 100644
index 0000000..e518942
--- /dev/null
+++ b/templates/base.tpl
@@ -0,0 +1,18 @@
+{{ define "base" }}
+<!doctype html>
+<html>
+<head>
+	<style type="text/css">
+	span.todo {
+		background: red;
+	}
+	</style>
+	<title>In Vino Veritas {{ block "title" . }}{{ end }}</title>
+</head>
+<body>
+{{ block "content" . }}
+TODO
+{{ end }}
+</body>
+</html>
+{{ end }}
\ No newline at end of file
diff --git a/templates/details.tpl b/templates/details.tpl
new file mode 100644
index 0000000..cf4d4e2
--- /dev/null
+++ b/templates/details.tpl
@@ -0,0 +1,9 @@
+{{ template "base" . }}
+
+{{ define "title" }}Details for {{ .Wine.Name }}{{ end }}
+
+{{ define "content" }}
+	{{ with .Wine }}
+		{{ .ID }} - {{ .Name }} - {{ .Rating }}
+	{{ end }}
+{{ end }}
\ No newline at end of file
diff --git a/templates/index.tpl b/templates/index.tpl
index 3996bd4..d087e16 100644
--- a/templates/index.tpl
+++ b/templates/index.tpl
@@ -19,7 +19,7 @@
 		{{ range .Wines }}
 			<tr>
 				<td>{{ .ID }}</td>
-				<td><a href="/details/{{ .ID }}">{{ .Name }}</a></td>
+				<td><a href="/details?id={{ .ID }}">{{ .Name }}</a></td>
 				<td>{{ .Rating }}</td>
 			</tr>
 		{{ end }}
diff --git a/vino.go b/vino.go
index 0a4cb6c..90c34f7 100644
--- a/vino.go
+++ b/vino.go
@@ -16,6 +16,29 @@ type Vino struct {
 	Picture []byte `db:"picture"`
 }
 
+func LoadVino(ctx context.Context, db *sqlx.DB, id int, withImage bool) (Vino, error) {
+	cols := []string{"rowid", "name", "rating"}
+	if withImage {
+		cols = append(cols, "picture")
+	}
+
+	query, args, err := squirrel.Select(cols...).
+		From("wines").
+		Where(squirrel.Eq{"rowid": id}).
+		ToSql()
+	if err != nil {
+		return Vino{}, err
+	}
+
+	var v Vino
+	err = db.GetContext(ctx, &v, query, args...)
+	if err != nil {
+		return v, err
+	}
+
+	return v, nil
+}
+
 func (v Vino) String() string {
 	return fmt.Sprintf("{Name: %q, Rating: %d}", v.Name, v.Rating)
 }
-- 
GitLab