aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPřemysl Janouch <p@janouch.name>2019-04-14 03:59:53 +0200
committerPřemysl Janouch <p@janouch.name>2019-04-14 10:17:02 +0200
commite003427f9f86b0b3898cca67b39a96e391fd1b16 (patch)
treedef257b866fef245792d50c621af529773cfa381
parent7eb84cd937ad65b0941164c5efe6b35a1210f8c3 (diff)
downloadsklad-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.tmpl44
-rw-r--r--sklad/container.tmpl86
-rw-r--r--sklad/db.go11
-rw-r--r--sklad/login.tmpl5
-rw-r--r--sklad/main.go110
-rw-r--r--sklad/series.tmpl43
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 }}&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 }}
-<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 }} &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>
-{{ 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 }}&amp;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, &params)
}
-// 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, &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() {
// 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 }}&amp;remove">
+ <input type=submit value="Odstranit">
+ </form>
+</header>
+</section>
+{{ else }}
+<p>Nejsou žádné řady.
+{{ end }}
+
+{{ end }}
+
+{{ end }}