sklad: preliminary web interface

Only exposing most read operations thus far.
This commit is contained in:
Přemysl Eric Janouch 2019-04-14 03:59:53 +02:00
parent 7eb84cd937
commit e003427f9f
Signed by: p
GPG Key ID: A0420B94F92B9493
6 changed files with 254 additions and 47 deletions

View File

@ -1,19 +1,46 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<title>{{ template "Title" }} - sklad</title> <title>{{ template "Title" . }} - sklad</title>
<meta http-equiv=Content-Type content="text/html; charset=utf-8"> <meta http-equiv=Content-Type content="text/html; charset=utf-8">
<meta name=viewport content="width=device-width, initial-scale=1">
<style> <style>
html, body { min-height: 100vh; } html, body { min-height: 100vh; }
body { padding: 1em; box-sizing: border-box; body { padding: 1rem; box-sizing: border-box;
margin: 0 auto; max-width: 50em; margin: 0 auto; max-width: 50rem;
border-left: 1px solid #ccc; border-right: 1px solid #ccc; border-left: 1px solid #ccc; border-right: 1px solid #ccc;
font-family: sans-serif; } 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; section { border: 1px outset #ccc; padding: 0 .5rem; margin: 1rem 0; }
background: linear-gradient(0deg, #fff, #eee); } section > p { margin: 0 0 .5rem 0; }
header * { display: inline-block; }
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; } a { color: inherit; }
textarea { padding: .5rem; box-sizing: border-box; width: 100%;
font-family: inherit; resize: vertical; }
select { max-width: 15rem; }
</style> </style>
</head> </head>
<body> <body>
@ -21,7 +48,7 @@
<header> <header>
<h1>sklad</h1> <h1>sklad</h1>
{{ if .LoggedIn }} {{ block "HeaderControls" . }}
<a href=/>Obaly</a> <a href=/>Obaly</a>
<a href=/series>Řady</a> <a href=/series>Řady</a>
@ -33,6 +60,7 @@
<input type=submit value="Odhlásit"> <input type=submit value="Odhlásit">
</form> </form>
{{ end }} {{ end }}
</header> </header>
{{ template "Content" . }} {{ template "Content" . }}

View File

@ -1,25 +1,91 @@
{{ define "Title" }}Přehled{{ end }} {{ define "Title" }}{{ or .Id "Obaly" }}{{ end }}
{{ define "Content" }} {{ define "Content" }}
{{ if .Id }} {{ 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 }}&amp;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 }} &mdash; {{ $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 }} {{ else }}
<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 }} &mdash; {{ $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>
<h2>Obaly nejvyšší úrovně</h2> <h2>Obaly nejvyšší úrovně</h2>
{{ end }} {{ end }}
{{ if .Description }}
<p>{{ .Description }}
{{ end }}
{{ if .Children }}
{{ range .Children }} {{ range .Children }}
<fieldset> <section>
<h3><a href="/container?id={{ .Id }}">{{ .Id }}</a></h3> <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 }}&amp;remove">
<input type=submit value="Odstranit">
</form>
</header>
{{ if .Description }} {{ if .Description }}
<p>{{ .Description }} <p>{{ .Description }}
{{ end }} {{ end }}
</fieldset> </section>
{{ end }}
{{ else }} {{ else }}
<p>Obal je prázdný. <p>Obal je prázdný.
{{ end }} {{ end }}

View File

@ -123,13 +123,12 @@ func loadDatabase() error {
// Construct an index that goes from parent containers to their children. // Construct an index that goes from parent containers to their children.
for _, pv := range db.Containers { for _, pv := range db.Containers {
if pv.Parent == "" { if pv.Parent != "" {
continue
}
if _, ok := indexContainer[pv.Parent]; !ok { if _, ok := indexContainer[pv.Parent]; !ok {
return fmt.Errorf("container %s has a nonexistent parent %s", return fmt.Errorf("container %s has a nonexistent parent %s",
pv.Id(), pv.Parent) pv.Id(), pv.Parent)
} }
}
indexChildren[pv.Parent] = append(indexChildren[pv.Parent], pv) indexChildren[pv.Parent] = append(indexChildren[pv.Parent], pv)
} }

View File

@ -1,12 +1,13 @@
{{ define "Title" }}Přihlášení{{ end }} {{ define "Title" }}Přihlášení{{ end }}
{{ define "HeaderControls" }}<!-- text/template requires content -->{{ end }}
{{ define "Content" }} {{ define "Content" }}
<h2>Přihlášení</h2> <h2>Přihlášení</h2>
<form method=post> <form method=post>
<label for=password>Heslo:</label> <label for=password>Heslo:</label>
<input type=password name=password id=password> <input type=password name=password id=password
<input type=submit value="Přihlásit"> ><input type=submit value="Přihlásit">
</form> </form>
{{ if .IncorrectPassword }} {{ if .IncorrectPassword }}

View File

@ -13,8 +13,6 @@ import (
var templates = map[string]*template.Template{} 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{}) { func executeTemplate(name string, w io.Writer, data interface{}) {
if err := templates[name].Execute(w, data); err != nil { if err := templates[name].Execute(w, data); err != nil {
panic(err) panic(err)
@ -48,7 +46,6 @@ func handleLogin(w http.ResponseWriter, r *http.Request) {
} }
params := struct { params := struct {
LoggedIn bool
IncorrectPassword bool IncorrectPassword bool
}{} }{}
@ -82,39 +79,119 @@ func handleLogout(w http.ResponseWriter, r *http.Request) {
} }
func handleContainer(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 { if r.Method != http.MethodGet {
w.WriteHeader(http.StatusMethodNotAllowed) w.WriteHeader(http.StatusMethodNotAllowed)
return return
} }
allSeries := map[string]string{}
for _, s := range indexSeries {
allSeries[s.Prefix] = s.Description
}
children := []*Container{} children := []*Container{}
id := ContainerId(r.FormValue("id")) id := ContainerId(r.FormValue("id"))
description := "" description := ""
series := ""
parent := ContainerId("")
if id == "" { if id == "" {
children = db.Containers children = indexChildren[id]
} else if container, ok := indexContainer[id]; ok { } else if container, ok := indexContainer[id]; ok {
children = indexChildren[id] children = indexChildren[id]
description = container.Description description = container.Description
series = container.Series
parent = container.Parent
} }
params := struct { params := struct {
LoggedIn bool
Id ContainerId Id ContainerId
Description string Description string
Children []*Container Children []*Container
Series string
Parent ContainerId
AllSeries map[string]string
}{ }{
LoggedIn: true,
Id: id, Id: id,
Description: description, Description: description,
Children: children, Children: children,
Series: series,
Parent: parent,
AllSeries: allSeries,
} }
executeTemplate("container.tmpl", w, &params) executeTemplate("container.tmpl", w, &params)
} }
// TODO: Consider a wrapper function that automatically calls ParseForm func handleSeries(w http.ResponseWriter, r *http.Request) {
// and disables client-side caching. 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, &params)
}
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, &params)
}
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, &params)
}
func main() { func main() {
// Randomize the RNG for session string generation. // 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. // TODO: Eventually we will need to load a font file for label printing.
// - The path might be part of configuration, or implicit by filename. // - 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("/login", wrap(handleLogin))
http.HandleFunc("/logout", sessionWrap(wrap(handleLogout))) 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)) log.Fatalln(http.ListenAndServe(address, nil))
} }

43
sklad/series.tmpl Normal file
View File

@ -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 }}&amp;remove">
<input type=submit value="Odstranit">
</form>
</header>
</section>
{{ else }}
<p>Nejsou žádné řady.
{{ end }}
{{ end }}
{{ end }}