diff options
author | Přemysl Janouch <p@janouch.name> | 2019-04-14 03:59:53 +0200 |
---|---|---|
committer | Přemysl Janouch <p@janouch.name> | 2019-04-14 10:17:02 +0200 |
commit | e003427f9f86b0b3898cca67b39a96e391fd1b16 (patch) | |
tree | def257b866fef245792d50c621af529773cfa381 | |
parent | 7eb84cd937ad65b0941164c5efe6b35a1210f8c3 (diff) | |
download | sklad-e003427f9f86b0b3898cca67b39a96e391fd1b16.tar.gz sklad-e003427f9f86b0b3898cca67b39a96e391fd1b16.tar.xz sklad-e003427f9f86b0b3898cca67b39a96e391fd1b16.zip |
sklad: preliminary web interface
Only exposing most read operations thus far.
-rw-r--r-- | sklad/base.tmpl | 44 | ||||
-rw-r--r-- | sklad/container.tmpl | 86 | ||||
-rw-r--r-- | sklad/db.go | 11 | ||||
-rw-r--r-- | sklad/login.tmpl | 5 | ||||
-rw-r--r-- | sklad/main.go | 110 | ||||
-rw-r--r-- | sklad/series.tmpl | 43 |
6 files changed, 253 insertions, 46 deletions
diff --git a/sklad/base.tmpl b/sklad/base.tmpl index b8487ee..542d019 100644 --- a/sklad/base.tmpl +++ b/sklad/base.tmpl @@ -1,19 +1,46 @@ <!DOCTYPE html> <html> <head> - <title>{{ template "Title" }} - sklad</title> + <title>{{ template "Title" . }} - sklad</title> <meta http-equiv=Content-Type content="text/html; charset=utf-8"> + <meta name=viewport content="width=device-width, initial-scale=1"> <style> html, body { min-height: 100vh; } - body { padding: 1em; box-sizing: border-box; - margin: 0 auto; max-width: 50em; + body { padding: 1rem; box-sizing: border-box; + margin: 0 auto; max-width: 50rem; border-left: 1px solid #ccc; border-right: 1px solid #ccc; font-family: sans-serif; } - header { display: flex; justify-content: space-between; align-items: center; - flex-wrap: wrap; margin: -1em -1em 0 -1em; padding: 0 1em; - background: linear-gradient(0deg, #fff, #eee); } - header * { display: inline-block; } + + section { border: 1px outset #ccc; padding: 0 .5rem; margin: 1rem 0; } + section > p { margin: 0 0 .5rem 0; } + + header, footer { display: flex; justify-content: space-between; + align-items: center; flex-wrap: wrap; padding-top: .5em; } + header { margin: 0 -.5rem; padding: .5rem .5rem 0 .5rem; + background: linear-gradient(0deg, transparent, #f8f8f8); } + body > header { margin: -1rem -1rem 0 -1rem; padding: 1rem 1rem 0 1rem; + background: linear-gradient(0deg, transparent, #eeeeee); } + + header *, + footer * { display: inline-block; } + header > *, + footer > * { margin: 0 0 .5rem 0; } + header > *:not(:last-child), + footer > *:not(:last-child) { margin-right: .5rem; } + + header > h2, + header > h3 { flex-grow: 1; } + + /* Don't ask me why this is an improvement on mobile browsers. */ + input[type=submit], input[type=text], input[type=password], + select, textarea { border: 1px inset #ccc; padding: .25rem; } + input[type=submit] { border-style: outset; } + select { border-style: solid; } + a { color: inherit; } + textarea { padding: .5rem; box-sizing: border-box; width: 100%; + font-family: inherit; resize: vertical; } + select { max-width: 15rem; } </style> </head> <body> @@ -21,7 +48,7 @@ <header> <h1>sklad</h1> -{{ if .LoggedIn }} +{{ block "HeaderControls" . }} <a href=/>Obaly</a> <a href=/series>Řady</a> @@ -33,6 +60,7 @@ <input type=submit value="Odhlásit"> </form> {{ end }} + </header> {{ template "Content" . }} diff --git a/sklad/container.tmpl b/sklad/container.tmpl index b261496..cbc8ea4 100644 --- a/sklad/container.tmpl +++ b/sklad/container.tmpl @@ -1,25 +1,91 @@ -{{ define "Title" }}Přehled{{ end }} +{{ define "Title" }}{{ or .Id "Obaly" }}{{ end }} {{ define "Content" }} {{ if .Id }} -<h2>{{ .Id }}</h2> + +<section> +<header> + <h2>{{ .Id }}</h2> + <form method=post action="/label?id={{ .Id }}"> + <input type=submit value="Vytisknout štítek"> + </form> + <form method=post action="/?id={{ .Id }}&remove"> + <input type=submit value="Odstranit"> + </form> +</header> + +<form method=post action="/?id={{ .Id }}"> +<textarea name=description rows=5> +{{ .Description }} +</textarea> +<footer> + <div> + <label for=series>Řada:</label> + <select name=series id=series> +{{ range $prefix, $desc := .AllSeries }} + <option value="{{ $prefix }}" + {{ if eq $prefix $.Series }}selected{{ end }} + >{{ $prefix }} — {{ $desc }}</option> +{{ end }} + </select> + </div> + <div> + <label for=parent>Nadobal:</label> + <input type=text name=parent id=parent value="{{ .Parent }}"> + </div> + <input type=submit value="Uložit"> +</footer> +</form> +</section> + +<h2>Podobaly</h3> + {{ else }} -<h2>Obaly nejvyšší úrovně</h2> +<section> +<header> + <h2>Nový obal</h2> +</header> +<form method=post action="/"> +<textarea name=description rows=5 + placeholder="Popis obalu nebo jeho obsahu"></textarea> +<footer> + <div> + <label for=series>Řada:</label> + <select name=series id=series> +{{ range $prefix, $desc := .AllSeries }} + <option value="{{ $prefix }}" + {{ if eq $prefix $.Series }}selected{{ end }} + >{{ $prefix }} — {{ $desc }}</option> {{ end }} + </select> + </div> + <div> + <label for=parent>Nadobal:</label> + <input type=text name=parent id=parent value=""> + </div> + <input type=submit value="Uložit"> +</footer> +</form> +</section> -{{ if .Description }} -<p>{{ .Description }} +<h2>Obaly nejvyšší úrovně</h2> {{ end }} -{{ if .Children }} {{ range .Children }} -<fieldset> -<h3><a href="/container?id={{ .Id }}">{{ .Id }}</a></h3> +<section> +<header> + <h3><a href="/container?id={{ .Id }}">{{ .Id }}</a></h3> + <form method=post action="/label?id={{ .Id }}"> + <input type=submit value="Vytisknout štítek"> + </form> + <form method=post action="/?id={{ .Id }}&remove"> + <input type=submit value="Odstranit"> + </form> +</header> {{ if .Description }} <p>{{ .Description }} {{ end }} -</fieldset> -{{ end }} +</section> {{ else }} <p>Obal je prázdný. {{ end }} diff --git a/sklad/db.go b/sklad/db.go index 8710fb5..3420c23 100644 --- a/sklad/db.go +++ b/sklad/db.go @@ -123,12 +123,11 @@ func loadDatabase() error { // Construct an index that goes from parent containers to their children. for _, pv := range db.Containers { - if pv.Parent == "" { - continue - } - if _, ok := indexContainer[pv.Parent]; !ok { - return fmt.Errorf("container %s has a nonexistent parent %s", - pv.Id(), pv.Parent) + if pv.Parent != "" { + if _, ok := indexContainer[pv.Parent]; !ok { + return fmt.Errorf("container %s has a nonexistent parent %s", + pv.Id(), pv.Parent) + } } indexChildren[pv.Parent] = append(indexChildren[pv.Parent], pv) } diff --git a/sklad/login.tmpl b/sklad/login.tmpl index 8dbca84..dab1172 100644 --- a/sklad/login.tmpl +++ b/sklad/login.tmpl @@ -1,12 +1,13 @@ {{ define "Title" }}Přihlášení{{ end }} +{{ define "HeaderControls" }}<!-- text/template requires content -->{{ end }} {{ define "Content" }} <h2>Přihlášení</h2> <form method=post> <label for=password>Heslo:</label> -<input type=password name=password id=password> -<input type=submit value="Přihlásit"> +<input type=password name=password id=password +><input type=submit value="Přihlásit"> </form> {{ if .IncorrectPassword }} diff --git a/sklad/main.go b/sklad/main.go index dee8723..a2a7143 100644 --- a/sklad/main.go +++ b/sklad/main.go @@ -13,8 +13,6 @@ import ( var templates = map[string]*template.Template{} -// TODO: Consider wrapping the data object in something that always contains -// a LoggedIn member, so that we don't need to duplicate it. func executeTemplate(name string, w io.Writer, data interface{}) { if err := templates[name].Execute(w, data); err != nil { panic(err) @@ -48,7 +46,6 @@ func handleLogin(w http.ResponseWriter, r *http.Request) { } params := struct { - LoggedIn bool IncorrectPassword bool }{} @@ -82,39 +79,119 @@ func handleLogout(w http.ResponseWriter, r *http.Request) { } func handleContainer(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodPost { + // TODO + } if r.Method != http.MethodGet { w.WriteHeader(http.StatusMethodNotAllowed) return } + allSeries := map[string]string{} + for _, s := range indexSeries { + allSeries[s.Prefix] = s.Description + } + children := []*Container{} id := ContainerId(r.FormValue("id")) description := "" + series := "" + parent := ContainerId("") if id == "" { - children = db.Containers + children = indexChildren[id] } else if container, ok := indexContainer[id]; ok { children = indexChildren[id] description = container.Description + series = container.Series + parent = container.Parent } params := struct { - LoggedIn bool Id ContainerId Description string Children []*Container + Series string + Parent ContainerId + AllSeries map[string]string }{ - LoggedIn: true, Id: id, Description: description, Children: children, + Series: series, + Parent: parent, + AllSeries: allSeries, } executeTemplate("container.tmpl", w, ¶ms) } -// TODO: Consider a wrapper function that automatically calls ParseForm -// and disables client-side caching. +func handleSeries(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodPost { + // TODO + } + if r.Method != http.MethodGet { + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + + allSeries := map[string]string{} + for _, s := range indexSeries { + allSeries[s.Prefix] = s.Description + } + + prefix := r.FormValue("prefix") + description := "" + + if prefix == "" { + } else if series, ok := indexSeries[prefix]; ok { + description = series.Description + } + + params := struct { + Prefix string + Description string + AllSeries map[string]string + }{ + Prefix: prefix, + Description: description, + AllSeries: allSeries, + } + + executeTemplate("series.tmpl", w, ¶ms) +} + +func handleSearch(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + + query := r.FormValue("q") + _ = query + + // TODO: Query the database for exact matches and fulltext. + + params := struct{}{} + + executeTemplate("search.tmpl", w, ¶ms) +} + +func handleLabel(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + + id := r.FormValue("id") + _ = id + + // TODO: See if such a container exists, print a label on the printer. + + params := struct{}{} + + executeTemplate("label.tmpl", w, ¶ms) +} func main() { // Randomize the RNG for session string generation. @@ -144,20 +221,13 @@ func main() { // TODO: Eventually we will need to load a font file for label printing. // - The path might be part of configuration, or implicit by filename. - // TODO: Some routing and pages. - // - // - GET /container?id=UA1 - // - GET /series?id=A - // - GET /search?q=bottle - // - // - https://stackoverflow.com/a/33880971/76313 - // - POST /label?id=UA1 - - http.HandleFunc("/", sessionWrap(wrap(handleContainer))) - http.HandleFunc("/container", sessionWrap(wrap(handleContainer))) - http.HandleFunc("/login", wrap(handleLogin)) http.HandleFunc("/logout", sessionWrap(wrap(handleLogout))) + http.HandleFunc("/", sessionWrap(wrap(handleContainer))) + http.HandleFunc("/series", sessionWrap(wrap(handleSeries))) + http.HandleFunc("/search", sessionWrap(wrap(handleSearch))) + http.HandleFunc("/label", sessionWrap(wrap(handleLabel))) + log.Fatalln(http.ListenAndServe(address, nil)) } diff --git a/sklad/series.tmpl b/sklad/series.tmpl new file mode 100644 index 0000000..4956e3a --- /dev/null +++ b/sklad/series.tmpl @@ -0,0 +1,43 @@ +{{ define "Title" }}{{ or .Prefix "Řady" }}{{ end }} +{{ define "Content" }} + +{{ if .Prefix }} +<h2>{{ .Prefix }}</h2> + +{{ if .Description }} +<p>{{ .Description }} +{{ end }} +{{ else }} + +<section> +<form method=post action="/series"> +<header> + <h3>Nová řada</h3> + <input type=text name=prefix placeholder="Prefix řady"> + <input type=text name=description placeholder="Popis řady" + ><input type=submit value="Uložit"> + </form> +</header> +</form> +</section> + +{{ range $prefix, $desc := .AllSeries }} +<section> +<header> + <h3><a href="/series?prefix={{ $prefix }}">{{ $prefix }}</a></h3> + <form method=post action="/series?prefix={{ $prefix }}"> + <input type=text name=description value="{{ $desc }}" + ><input type=submit value="Uložit"> + </form> + <form method=post action="/series?prefix={{ $prefix }}&remove"> + <input type=submit value="Odstranit"> + </form> +</header> +</section> +{{ else }} +<p>Nejsou žádné řady. +{{ end }} + +{{ end }} + +{{ end }} |