hot reload, search refactor begin

This commit is contained in:
Simon Martens
2025-02-25 19:23:00 +01:00
parent f35c738cee
commit 4d65b71563
24 changed files with 706 additions and 202 deletions

View File

@@ -8,6 +8,8 @@ full_bin = "./tmp/musenalm --dir ./pb_data serve"
cmd = "go build -tags=dev,fts5,sqlite_icu -o ./tmp/musenalm ."
delay = 400
exclude_dir = [
"views/routes",
"views/layouts",
"views/assets",
"views/node_modules",
"tmp",
@@ -32,7 +34,7 @@ log = "build-errors.log"
poll = false
poll_interval = 0
post_cmd = []
pre_cmd = ["npm --prefix views/ run build"]
pre_cmd = [""]
rerun = false
rerun_delay = 250
send_interrupt = true

View File

@@ -120,20 +120,21 @@ func (app *App) Serve() error {
// INFO: hot reloading for poor people
if app.MAConfig.Debug {
watcher, err := fsnotify.NewWatcher()
watcher, err := EngineWatcher(engine)
if err != nil {
return fmt.Errorf("Failed to create watcher: %w.", err)
app.PB.Logger().Error("Failed to create watcher, continuing without", "error", err)
} else {
watcher.AddRecursive(LAYOUT_DIR)
watcher.AddRecursive(ROUTES_DIR)
engine.Debug()
rwatcher, err := RefreshWatcher(engine)
if err != nil {
app.PB.Logger().Error("Failed to create watcher, continuing without", "error", err)
} else {
rwatcher.Add("./views/assets")
}
defer watcher.Close()
go app.watchFN(watcher, engine)
if err := watcher.Add(LAYOUT_DIR); err != nil {
return fmt.Errorf("Failed to watch layout directory: %w.", err)
}
if err := watcher.Add(ROUTES_DIR); err != nil {
return fmt.Errorf("Failed to watch routes directory: %w.", err)
}
}
app.PB.OnBootstrap().BindFunc(func(e *core.BootstrapEvent) error {

136
app/watch.go Normal file
View File

@@ -0,0 +1,136 @@
package app
import (
"io/fs"
"log"
"os"
"path/filepath"
"time"
"github.com/Theodor-Springmann-Stiftung/musenalm/templating"
"github.com/fsnotify/fsnotify"
)
const (
WATCHER_DEBOUNCE = 300 * time.Millisecond
)
// INFO: this is hot reload for poor people
type Watcher struct {
*fsnotify.Watcher
}
func RefreshWatcher(engine *templating.Engine) (*Watcher, error) {
watcher := Watcher{}
w, err := fsnotify.NewWatcher()
if err != nil {
return nil, err
}
watcher.Watcher = w
done := make(chan bool)
go func() {
var reloadTimer *time.Timer
for {
select {
case event := <-watcher.Events:
if event.Op&(fsnotify.Create|fsnotify.Write|fsnotify.Remove|fsnotify.Rename) != 0 {
if reloadTimer != nil {
reloadTimer.Stop()
}
reloadTimer = time.AfterFunc(WATCHER_DEBOUNCE, func() {
log.Println("Changes detected, reloading templates...")
engine.Refresh()
})
}
if event.Op&fsnotify.Create == fsnotify.Create {
fi, statErr := os.Stat(event.Name)
if statErr == nil && fi.IsDir() {
_ = watcher.Add(event.Name)
log.Printf("Now watching new directory: %s", event.Name)
}
}
case err := <-watcher.Errors:
if err != nil {
log.Printf("fsnotify error: %v\n", err)
}
case <-done:
watcher.Close()
return
}
}
}()
return &watcher, nil
}
func EngineWatcher(engine *templating.Engine) (*Watcher, error) {
watcher := Watcher{}
w, err := fsnotify.NewWatcher()
if err != nil {
return nil, err
}
watcher.Watcher = w
done := make(chan bool)
go func() {
var reloadTimer *time.Timer
for {
select {
case event := <-watcher.Events:
if event.Op&(fsnotify.Create|fsnotify.Write|fsnotify.Remove|fsnotify.Rename) != 0 {
if reloadTimer != nil {
reloadTimer.Stop()
}
reloadTimer = time.AfterFunc(WATCHER_DEBOUNCE, func() {
log.Println("Changes detected, reloading templates...")
engine.Reload()
})
}
if event.Op&fsnotify.Create == fsnotify.Create {
fi, statErr := os.Stat(event.Name)
if statErr == nil && fi.IsDir() {
_ = watcher.Add(event.Name)
log.Printf("Now watching new directory: %s", event.Name)
}
}
case err := <-watcher.Errors:
if err != nil {
log.Printf("fsnotify error: %v\n", err)
}
case <-done:
watcher.Close()
return
}
}
}()
return &watcher, nil
}
func (w *Watcher) AddRecursive(root string) error {
return filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() {
werr := w.Add(path)
if werr != nil {
return werr
}
log.Printf("Now watching directory: %s", path)
}
return nil
})
}

View File

@@ -22,6 +22,15 @@ func REntriesAgents_Agent(app core.App, id string) ([]*REntriesAgents, error) {
)
}
func REntriesAgents_Entry(app core.App, id string) ([]*REntriesAgents, error) {
return TableByField[[]*REntriesAgents](
app,
RelationTableName(ENTRIES_TABLE, AGENTS_TABLE),
ENTRIES_TABLE,
id,
)
}
func RContentsAgents_Agent(app core.App, id string) ([]*RContentsAgents, error) {
return TableByField[[]*RContentsAgents](
app,

View File

@@ -104,3 +104,13 @@ func (p *AlmanachPage) getAbbr(app core.App, data map[string]interface{}) error
func (p *AlmanachPage) Get(request *core.RequestEvent, engine *templating.Engine, data map[string]interface{}) error {
return engine.Response200(request, TEMPLATE_ALMANACH, data)
}
type AlmanachResult struct {
Entry *dbmodels.Entry
Places []*dbmodels.Place
Series []*dbmodels.Series
Contents []*dbmodels.Content
}
type AlmanachData struct {
}

View File

@@ -1,12 +1,13 @@
package pages
import (
"fmt"
"net/http"
"slices"
"strings"
"github.com/Theodor-Springmann-Stiftung/musenalm/app"
"github.com/Theodor-Springmann-Stiftung/musenalm/dbmodels"
"github.com/Theodor-Springmann-Stiftung/musenalm/helpers/datatypes"
"github.com/Theodor-Springmann-Stiftung/musenalm/pagemodels"
"github.com/Theodor-Springmann-Stiftung/musenalm/templating"
"github.com/pocketbase/pocketbase/core"
@@ -47,82 +48,41 @@ func (p *SuchePage) Setup(router *router.Router[*core.RequestEvent], app core.Ap
})
router.GET(URL_SUCHE, func(e *core.RequestEvent) error {
if !slices.Contains(availableTypes, e.Request.PathValue("type")) {
return engine.Response404(e, nil, nil)
paras, err := NewParameters(e)
if err != nil {
return engine.Response404(e, err, nil)
}
q := e.Request.URL.Query().Get(PARAM_QUERY)
q = strings.TrimSpace(q)
if q != "" {
return p.SimpleSearchRequest(app, engine, e)
}
// if e.Request.URL.Query().Get(PARAM_QUERY) != "" {
// return p.SearchRequest(app, engine, e)
// }
return p.DefaultRequest(app, engine, e)
data := make(map[string]interface{})
data["parameters"] = paras
return engine.Response200(e, p.Template+paras.Collection+"/", data, p.Layout)
})
return nil
}
func (p *SuchePage) SimpleSearchRequest(app core.App, engine *templating.Engine, e *core.RequestEvent) error {
t := e.Request.PathValue("type")
if t == "reihen" {
return p.SimpleSearchReihenRequest(app, engine, e)
}
if t == "baende" {
return p.SimpleSearchBaendeRequest(app, engine, e)
}
// if t == "beitraege" {
// return p.SimpleSearchBeitraegeRequest(app, engine, e)
// }
// if t == "personen" {
// return p.SimpleSearchPersonenRequest(app, engine, e)
// }
func (p *SuchePage) SimpleSearchReihenRequest(app core.App, engine *templating.Engine, e *core.RequestEvent) error {
return engine.Response404(e, nil, nil)
}
const (
REIHEN_PARAM_TITLE = "title"
REIHEN_PARAM_ANNOTATIONS = "annotations"
REIHEN_PARAM_REFERENCES = "references"
)
func (p *SuchePage) SimpleSearchBaendeRequest(app core.App, engine *templating.Engine, e *core.RequestEvent, pp Parameters) error {
data := make(map[string]interface{})
params, err := NewSimpleParameters(e, pp)
if err != nil {
return engine.Response404(e, err, nil)
}
func (p *SuchePage) SimpleSearchReihenRequest(app core.App, engine *templating.Engine, e *core.RequestEvent) error {
q := e.Request.URL.Query().Get(PARAM_QUERY)
data := p.CommonData(app, engine, e)
data["q"] = q
hasTitle := e.Request.URL.Query().Get(REIHEN_PARAM_TITLE) == "on"
hasAnnotations := e.Request.URL.Query().Get(REIHEN_PARAM_ANNOTATIONS) == "on"
hasReferences := e.Request.URL.Query().Get(REIHEN_PARAM_REFERENCES) == "on"
if !hasTitle && !hasAnnotations && !hasReferences {
query := dbmodels.NormalizeQuery(params.Query)
if len(query) == 0 {
engine.Response404(e, nil, nil)
}
fields := []string{}
options := map[string]bool{}
if hasTitle {
fields = append(fields, dbmodels.SERIES_TITLE_FIELD)
options[REIHEN_PARAM_TITLE] = true
}
if hasAnnotations {
fields = append(fields, dbmodels.ANNOTATION_FIELD)
options[REIHEN_PARAM_ANNOTATIONS] = true
}
if hasReferences {
fields = append(fields, dbmodels.REFERENCES_FIELD)
options[REIHEN_PARAM_REFERENCES] = true
}
data["options"] = options
query := dbmodels.NormalizeQuery(q)
if len(q) == 0 {
fields := params.FieldSetBaende()
if len(fields) == 0 {
return engine.Response404(e, nil, nil)
}
ids, err := dbmodels.FTS5Search(app, dbmodels.SERIES_TABLE, dbmodels.FTS5QueryRequest{
ids, err := dbmodels.FTS5Search(app, dbmodels.ENTRIES_TABLE, dbmodels.FTS5QueryRequest{
Fields: fields,
Query: query,
})
@@ -130,43 +90,20 @@ func (p *SuchePage) SimpleSearchReihenRequest(app core.App, engine *templating.E
return engine.Response500(e, err, nil)
}
idsany := []any{}
for _, id := range ids {
idsany = append(idsany, id.ID)
}
series, err := dbmodels.SeriessesForIds(app, idsany)
rmap, bmap, err := dbmodels.EntriesForSeriesses(app, series)
idsany := datatypes.ToAny(ids)
entries, err := dbmodels.Entries_IDs(app, idsany)
if err != nil {
return engine.Response500(e, err, nil)
}
dbmodels.Sort_Series_Title(series)
data["series"] = series
data["relations"] = rmap
data["entries"] = bmap
dbmodels.Sort_Entries_Title_Year(entries)
data["entries"] = entries
data["count"] = len(entries)
data["count"] = len(series)
// TODO: get relavant agents, years and places
return engine.Response200(e, p.Template, data, p.Layout)
}
const (
BAENDE_PARAM_ALM_NR = "alm"
BAENDE_PARAM_TITLE = "title"
BAENDE_PARAM_SERIES = "series"
BAENDE_PARAM_PERSONS = "persons"
BAENDE_PARAM_PLACES = "places"
BAENDE_PARAM_REFS = "references"
BAENDE_PARAM_ANNOTATIONS = "annotations"
// INFO: this is expanded search only:
BAENDE_PARAM_PSEUDONYMS = "pseudonyms"
// INFO: this is a filter type & expanded search:
BAENDE_PARAM_STATE = "state" // STATE: "full" "partial" "none"
)
func (p *SuchePage) SimpleSearchBaendeRequest(app core.App, engine *templating.Engine, e *core.RequestEvent) error {
eids := []any{}
for _, entry := range entries {
eids = append(eids, entry.Id)
}
return engine.Response404(e, nil, nil)
}
@@ -182,19 +119,125 @@ const (
// INFO: these are filter types & expanded search:
BEITRAEGE_PARAM_TYPE = "type"
BEITRAEGE_PARAM_SCANS = "scans"
REIHEN_PARAM_TITLE = "title"
REIHEN_PARAM_ANNOTATIONS = "annotations"
REIHEN_PARAM_REFERENCES = "references"
BAENDE_PARAM_ALM_NR = "alm"
BAENDE_PARAM_TITLE = "title"
BAENDE_PARAM_SERIES = "series"
BAENDE_PARAM_PERSONS = "persons"
BAENDE_PARAM_PLACES = "pubdata"
BAENDE_PARAM_REFS = "references"
BAENDE_PARAM_ANNOTATIONS = "annotations"
BAENDE_PARAM_YEAR = "year"
// INFO: this is expanded search only:
BAENDE_PARAM_PSEUDONYMS = "pseudonyms"
// INFO: this is a filter type & expanded search:
BAENDE_PARAM_STATE = "state" // STATE: "full" "partial" "none"
)
func (p *SuchePage) DefaultRequest(app core.App, engine *templating.Engine, e *core.RequestEvent) error {
data := p.CommonData(app, engine, e)
return engine.Response200(e, p.Template, data, p.Layout)
var ErrInvalidCollectionType = fmt.Errorf("Invalid collection type")
var ErrNoQuery = fmt.Errorf("No query")
type Parameters struct {
Extended bool
Collection string
Query string
}
func (p *SuchePage) CommonData(app core.App, engine *templating.Engine, e *core.RequestEvent) map[string]interface{} {
data := map[string]interface{}{}
data["type"] = e.Request.PathValue("type")
data[PARAM_EXTENDED] = false
if e.Request.URL.Query().Get(PARAM_EXTENDED) == "true" {
data[PARAM_EXTENDED] = true
func NewParameters(e *core.RequestEvent) (*Parameters, error) {
collection := e.Request.PathValue("type")
if !slices.Contains(availableTypes, collection) {
return nil, ErrInvalidCollectionType
}
return data
extended := false
if e.Request.URL.Query().Get(PARAM_EXTENDED) == "true" {
extended = true
}
return &Parameters{
Collection: collection,
Extended: extended,
Query: e.Request.URL.Query().Get(PARAM_QUERY),
}, nil
}
type SimpleParameters struct {
Parameters
Annotations bool
Persons bool
Title bool
Alm bool
Series bool
Places bool
Refs bool
Year bool
}
func NewSimpleParameters(e *core.RequestEvent, p Parameters) (*SimpleParameters, error) {
q := e.Request.URL.Query().Get(PARAM_QUERY)
if q == "" {
return nil, ErrNoQuery
}
alm := e.Request.URL.Query().Get(BAENDE_PARAM_ALM_NR) == "on"
title := e.Request.URL.Query().Get(BAENDE_PARAM_TITLE) == "on"
series := e.Request.URL.Query().Get(BAENDE_PARAM_SERIES) == "on"
persons := e.Request.URL.Query().Get(BAENDE_PARAM_PERSONS) == "on"
places := e.Request.URL.Query().Get(BAENDE_PARAM_PLACES) == "on"
refs := e.Request.URL.Query().Get(BAENDE_PARAM_REFS) == "on"
annotations := e.Request.URL.Query().Get(BAENDE_PARAM_ANNOTATIONS) == "on"
year := e.Request.URL.Query().Get(BAENDE_PARAM_YEAR) == "on"
// TODO: sanity check here if any single field is selected
return &SimpleParameters{
Parameters: p,
Alm: alm,
Title: title,
Series: series,
Persons: persons,
Places: places,
Refs: refs,
Annotations: annotations,
Year: year,
}, nil
}
func (p SimpleParameters) FieldSetBaende() []string {
fields := []string{}
if p.Alm {
fields = append(fields, dbmodels.MUSENALMID_FIELD)
}
if p.Title {
fields = append(fields,
dbmodels.TITLE_STMT_FIELD,
dbmodels.SUBTITLE_STMT_FIELD,
dbmodels.INCIPIT_STMT_FIELD,
dbmodels.VARIANT_TITLE_FIELD,
dbmodels.PARALLEL_TITLE_FIELD,
)
}
if p.Series {
fields = append(fields, dbmodels.SERIES_TABLE)
}
if p.Persons {
fields = append(fields, dbmodels.RESPONSIBILITY_STMT_FIELD, dbmodels.AGENTS_TABLE)
}
if p.Places {
fields = append(fields, dbmodels.PLACE_STMT_FIELD, dbmodels.PLACES_TABLE, dbmodels.PUBLICATION_STMT_FIELD)
}
if p.Refs {
fields = append(fields, dbmodels.REFERENCES_FIELD)
}
if p.Annotations {
fields = append(fields, dbmodels.ANNOTATION_FIELD)
}
if p.Year {
fields = append(fields, dbmodels.YEAR_FIELD)
}
return fields
}

View File

@@ -5,20 +5,64 @@ import (
"html/template"
"io"
"io/fs"
"log"
"net/http"
"strings"
"sync"
"github.com/Theodor-Springmann-Stiftung/musenalm/helpers/functions"
"github.com/pocketbase/pocketbase/core"
"golang.org/x/net/websocket"
)
const (
ASSETS_URL_PREFIX = "/assets"
RELOAD_TEMPLATE = `
<script type="module">
(function () {
let relto = -1;
const scheme = location.protocol === "https:" ? "wss" : "ws";
// Hardcode port 9000 here:
const url = scheme + "://" + location.hostname + ":9000/pb/reload";
function connect() {
const socket = new WebSocket(url);
socket.addEventListener("open", function () {
console.log("Reload socket connected (port 9000).");
});
socket.addEventListener("message", function (evt) {
if (evt.data === "reload") {
console.log("Received reload signal. Reloading...");
if (relto !== -1) clearTimeout(relto);
relto = setTimeout(() => location.reload(), 0);
}
});
socket.addEventListener("close", function () {
console.log("Reload socket closed. Reconnecting in 3 seconds...");
setTimeout(connect, 3000);
});
socket.addEventListener("error", function (err) {
console.error("Reload socket error:", err);
// We'll let onclose handle reconnection.
});
}
// Initiate the first connection attempt.
connect();
})();
</script>
`
)
type Engine struct {
regmu *sync.Mutex
debug bool
ws *WsServer
onceWS sync.Once
// NOTE: LayoutRegistry and TemplateRegistry have their own syncronization & cache and do not require a mutex here
LayoutRegistry *LayoutRegistry
@@ -44,6 +88,26 @@ func NewEngine(layouts, templates *fs.FS) *Engine {
return &e
}
func (e *Engine) Debug() {
e.debug = true
e.onceWS.Do(func() {
e.ws = NewWsServer()
go e.startWsServerOnPort9000()
})
}
func (e *Engine) startWsServerOnPort9000() {
// We'll create a basic default mux here and mount /pb/reload
mux := http.NewServeMux()
mux.Handle("/pb/reload", websocket.Handler(e.ws.Handler))
log.Println("[Engine Debug] Starting separate WebSocket server on :9000 for live reload...")
if err := http.ListenAndServe(":9000", mux); err != nil {
log.Println("[Engine Debug] WebSocket server error:", err)
}
}
func (e *Engine) funcs() error {
e.mu.Lock()
e.mu.Unlock()
@@ -113,6 +177,12 @@ func (e *Engine) Reload() {
e.Load()
}
func (e *Engine) Refresh() {
if e.debug && e.ws != nil {
e.ws.BroadcastReload()
}
}
// INFO: fn is a function that returns either one value or two values, the second one being an error
func (e *Engine) AddFunc(name string, fn interface{}) {
e.mu.Lock()
@@ -234,7 +304,15 @@ func (e *Engine) Response200(request *core.RequestEvent, path string, ld map[str
return e.Response500(request, err, ld)
}
return request.HTML(http.StatusOK, builder.String())
tstring := builder.String()
if e.debug {
idx := strings.LastIndex(tstring, "</body>")
if idx != -1 {
tstring = tstring[:idx] + RELOAD_TEMPLATE + tstring[idx:]
}
}
return request.HTML(http.StatusOK, tstring)
}
func requestData(request *core.RequestEvent) map[string]interface{} {

57
templating/ws.go Normal file
View File

@@ -0,0 +1,57 @@
package templating
import (
"log"
"sync"
"golang.org/x/net/websocket"
)
// WsServer manages all active WebSocket connections so we can broadcast.
type WsServer struct {
mu sync.Mutex
conns map[*websocket.Conn]bool
}
// NewWsServer creates a WsServer.
func NewWsServer() *WsServer {
return &WsServer{
conns: make(map[*websocket.Conn]bool),
}
}
// Handler is invoked for each new WebSocket connection.
func (s *WsServer) Handler(conn *websocket.Conn) {
s.mu.Lock()
s.conns[conn] = true
s.mu.Unlock()
log.Println("[WsServer] Connected:", conn.RemoteAddr())
// Read in a loop until an error (client disconnect).
var msg string
for {
if err := websocket.Message.Receive(conn, &msg); err != nil {
log.Println("[WsServer] Disconnected:", conn.RemoteAddr())
s.mu.Lock()
delete(s.conns, conn)
s.mu.Unlock()
conn.Close()
return
}
}
}
// BroadcastReload sends a "reload" message to all connected clients.
func (s *WsServer) BroadcastReload() {
s.mu.Lock()
defer s.mu.Unlock()
for conn := range s.conns {
err := websocket.Message.Send(conn, "reload")
if err != nil {
log.Println("[WsServer] Broadcast error:", err)
conn.Close()
delete(s.conns, conn)
}
}
}

62
views/.air.toml Normal file
View File

@@ -0,0 +1,62 @@
root = "."
testdata_dir = "testdata"
tmp_dir = "tmp"
[build]
args_bin = []
full_bin = ""
cmd = "npm run build"
delay = 400
exclude_dir = [
"assets",
"node_modules",
"tmp",
"vendor",
"testdata",
"data_git",
"cache_gnd",
"cache_geonames",
"pb_data",
"Almanach-Bilder",
"Static-Bilder",
]
exclude_file = []
exclude_regex = ["_test.go"]
exclude_unchanged = false
follow_symlink = false
include_dir = []
include_ext = ["go", "tpl", "tmpl", "html", "gohtml", "js", "css", "xsl"]
include_file = []
kill_delay = "0s"
log = "build-errors.log"
poll = false
poll_interval = 0
post_cmd = []
pre_cmd = [""]
rerun = false
rerun_delay = 250
send_interrupt = true
stop_on_error = true
[color]
app = ""
build = "yellow"
main = "magenta"
runner = "green"
watcher = "cyan"
[log]
main_only = false
time = false
[misc]
clean_on_exit = true
[proxy]
app_port = 8090
enabled = false
proxy_port = 8081
[screen]
clear_on_rebuild = true
keep_scroll = true

File diff suppressed because one or more lines are too long

View File

@@ -1,5 +1,4 @@
{{ $model := . }}
{{ if and .startpage .record }}
{{ template "hero" . }}
{{ end }}
@@ -17,7 +16,7 @@
{{ template "alphabet" Dict "active" .letter "letters" .letters "search" .search }}
</div>
{{ else }}
<div class="mt-2 border-b border-zinc-300 w-full"></div>
<div class="mt-2 border-b w-full"></div>
{{ end }}
</div>
@@ -42,7 +41,6 @@
{{ end }}
</div>
{{ end }}
{{ if .series }}
<div class="mb-1 max-w-[60rem] hyphens-auto">
{{ range $id, $r := .series }}

View File

@@ -0,0 +1,61 @@
{{ $model := . }}
<div id="searchcontrol" class="container-normal">
{{- template "_heading" $model.parameters.Parameters -}}
<div id="" class="border-l border-zinc-300 px-8 py-10 relative">
{{- if not $model.parameters.Extended -}}
<form
id="searchform"
class="w-full font-serif"
method="get"
action="/suche/baende"
autocomplete="off">
<div class="searchformcolumn">
{{- $q := "" }}
{{- if $model.parameters.Query -}}
{{- q = $model.parameters.Query -}}
{{- end -}}
{{ template "_searchboxsimple" Arr $model.parameters.Parameters true $q }}
<fieldset class="selectgroup">
<div class="selectgroup-option">
<input type="checkbox" name="alm" id="alm" checked />
<label for="alm">Almanach-Nr.</label>
</div>
<div class="selectgroup-option">
<input type="checkbox" name="title" id="title" checked />
<label for="title">Titel</label>
</div>
<div class="selectgroup-option">
<input type="checkbox" name="series" id="series" checked />
<label for="series">Reihentitel</label>
</div>
<div class="selectgroup-option">
<input type="checkbox" name="persons" id="persons" checked />
<label for="persons">Personen &amp; Verlage</label>
</div>
<div class="selectgroup-option">
<input type="checkbox" name="pubdata" id="pubdata" checked />
<label for="pubdata">Orte</label>
</div>
<div class="selectgroup-option">
<input type="checkbox" name="year" id="year" checked />
<label for="year">Jahr</label>
</div>
<div class="selectgroup-option">
<input type="checkbox" name="references" id="references" checked />
<label for="references">Nachweise</label>
</div>
<div class="selectgroup-option">
<input type="checkbox" name="annotations" id="annotations" checked />
<label for="annotations">Anmerkungen</label>
</div>
</fieldset>
{{ template "infotextsimple" true }}
</div>
</form>
</div>
</div>
{{- template "_fieldscript" -}}

View File

@@ -0,0 +1 @@
<title>{{ .site.title }}: Suche &ndash; Bände</title>

View File

@@ -0,0 +1,48 @@
{{ $model := . }}
<div id="searchcontrol" class="container-normal">
{{- template "_heading" $model.parameters.Parameters -}}
<div id="" class="border-l border-zinc-300 px-8 py-10 relative">
{{- if not $model.parameters.Extended -}}
<form
id="searchform"
class="w-full font-serif"
method="get"
action="/suche/baende"
autocomplete="off">
<div class="searchformcolumn">
{{- $q := "" }}
{{- if $model.parameters.Query -}}
{{- q = $model.parameters.Query -}}
{{- end -}}
{{ template "_searchboxsimple" Arr $model.parameters.Parameters true $q }}
<fieldset class="selectgroup">
<div class="selectgroup-option">
<input type="checkbox" name="number" id="number" checked />
<label for="number">Almanach-Nr.</label>
</div>
<div class="selectgroup-option">
<input type="checkbox" name="title" id="title" checked />
<label for="title">Titelinformationen</label>
</div>
<div class="selectgroup-option">
<input type="checkbox" name="entry" id="entry" checked />
<label for="entry">Bandtitel</label>
</div>
<div class="selectgroup-option">
<input type="checkbox" name="person" id="person" checked />
<label for="person">Personen &amp; Pseudonyme</label>
</div>
<div class="selectgroup-option">
<input type="checkbox" name="annotations" id="annotations" checked />
<label for="annotations">Anmerkungen</label>
</div>
</fieldset>
{{ template "infotextsimple" true }}
</div>
</form>
</div>
</div>
{{- template "_fieldscript" -}}

View File

@@ -0,0 +1 @@
<title>{{ .site.title }}: Suche &ndash; Beiträge</title>

View File

@@ -0,0 +1,17 @@
<script type="module">
let fieldset = document.querySelector("fieldset.selectgroup");
let checkboxes = Array.from(fieldset.querySelectorAll('input[type="checkbox"]'));
fieldset.addEventListener("change", (event) => {
let target = event.target;
if (target.type === "checkbox") {
let name = target.name;
let checked = target.checked;
if (!checked) {
let allchecked = checkboxes.filter((checkbox) => checkbox.checked);
if (allchecked.length === 0) {
target.checked = true;
}
}
}
});
</script>

View File

@@ -0,0 +1,53 @@
{{- $model := . -}}
<div id="searchheading" class="flex flex-row justify-between min-h-14 items-end relative">
<nav id="searchnav" class="flex flex-row items-end">
<div
class="align-bottom text-lg h-min self-end pb-0.5 italic font-bold
text-zinc-800">
Suche nach:
</div>
<!--
<a
href="/suche/reihen"
class="block no-underline"
{{ if eq $model.Collection "reihen" }}aria-current="page"{{- end -}}
>Reihen</a
>
-->
<a
href="/suche/baende"
class="block no-underline"
{{ if eq $model.Collection "baende" }}aria-current="page"{{- end -}}
>Bänden</a
>
<a
href="/suche/beitraege"
class="block no-underline"
{{ if eq $model.Collection "beitraege" }}aria-current="page"{{- end -}}
>Beiträgen</a
>
<!--
<a
href="/suche/personen"
class="block no-underline"
{{ if eq $model.Collection "personen" }}aria-current="page"{{- end -}}
>Personen</a
>
-->
</nav>
<h1
class="text-3xl font-bold px-3 relative translate-y-[45%] w-min whitespace-nowrap
bg-stone-50 mr-24 z-20">
Suche&nbsp;&middot;&nbsp;<span class="">
{{- if eq $model.Collection "reihen" -}}
Reihen
{{- else if eq $model.Collection "personen" -}}
Personen &amp; Körperschaften
{{- else if eq $model.Collection "baende" -}}
Bände
{{- else if eq $model.Collection "beitraege" -}}
Beiträge
{{- end -}}
</span>
</h1>
</div>

View File

@@ -1,10 +1,11 @@
{{ $model := index . 0 }}
{{ $parameters := index . 0 }}
{{ $extendable := index . 1 }}
{{ $q := index . 2 }}
<label for="q" class="hidden">Suchbegriffe</label>
<input
{{ if $model.q }}value="{{ $model.q }}"{{- end -}}
{{ if $q }}value="{{ $q }}"{{- end -}}
type="search"
name="q"
minlength="3"
@@ -16,7 +17,7 @@
{{ if $extendable }}
<a
href="/suche/{{ $model.type }}?extended=true"
href="/suche/{{ $parameters.Collection }}?extended=true"
class="whitespace-nowrap self-end block col-span-2">
<i class="ri-arrow-right-long-line"></i> Erweiterte Suche
</a>

View File

@@ -97,44 +97,6 @@
{{- else if eq $model.type "baende" -}}
<!-- INFO: Bände -->
{{- if not $model.extended -}}
<div class="grid grid-cols-12 gap-y-3 w-full gap-x-4">
{{ template "searchboxsimple" Arr . true }}
<fieldset class="selectgroup">
<div class="selectgroup-option">
<input type="checkbox" name="number" id="number" checked />
<label for="number">Almanach-Nr.</label>
</div>
<div class="selectgroup-option">
<input type="checkbox" name="title" id="title" checked />
<label for="title">Titel</label>
</div>
<div class="selectgroup-option">
<input type="checkbox" name="series" id="series" checked />
<label for="series">Reihentitel</label>
</div>
<div class="selectgroup-option">
<input type="checkbox" name="person" id="person" checked />
<label for="person">Personen &amp; Verlage</label>
</div>
<div class="selectgroup-option">
<input type="checkbox" name="pubdata" id="pubdata" checked />
<label for="pubdata">Orte</label>
</div>
<div class="selectgroup-option">
<input type="checkbox" name="year" id="year" checked />
<label for="year">Jahr</label>
</div>
<div class="selectgroup-option">
<input type="checkbox" name="references" id="references" checked />
<label for="references">Nachweise</label>
</div>
<div class="selectgroup-option">
<input type="checkbox" name="annotations" id="annotations" checked />
<label for="annotations">Anmerkungen</label>
</div>
</fieldset>
{{ template "infotextsimple" true }}
</div>
{{- else -}}
Extended search Bände
{{- end -}}
@@ -144,28 +106,6 @@
{{- if not $model.extended -}}
<div class="grid grid-cols-12 gap-y-3 w-full gap-x-4">
{{ template "searchboxsimple" Arr . true }}
<fieldset class="selectgroup">
<div class="selectgroup-option">
<input type="checkbox" name="number" id="number" checked />
<label for="number">Almanach-Nr.</label>
</div>
<div class="selectgroup-option">
<input type="checkbox" name="title" id="title" checked />
<label for="title">Titelinformationen</label>
</div>
<div class="selectgroup-option">
<input type="checkbox" name="entry" id="entry" checked />
<label for="entry">Bandtitel</label>
</div>
<div class="selectgroup-option">
<input type="checkbox" name="person" id="person" checked />
<label for="person">Personen &amp; Pseudonyme</label>
</div>
<div class="selectgroup-option">
<input type="checkbox" name="annotations" id="annotations" checked />
<label for="annotations">Anmerkungen</label>
</div>
</fieldset>
{{ template "infotextsimple" true }}
</div>
{{- else -}}
@@ -173,21 +113,3 @@
{{- end -}}
{{- end -}}
</form>
<script type="module">
let fieldset = document.querySelector("fieldset.selectgroup");
let checkboxes = Array.from(fieldset.querySelectorAll('input[type="checkbox"]'));
fieldset.addEventListener("change", (event) => {
let target = event.target;
if (target.type === "checkbox") {
let name = target.name;
let checked = target.checked;
if (!checked) {
let allchecked = checkboxes.filter((checkbox) => checkbox.checked);
if (allchecked.length === 0) {
target.checked = true;
}
}
}
});
</script>

View File

@@ -1 +0,0 @@
<title>{{ .site.title }}: Suche</title>

View File

@@ -0,0 +1 @@
<title>{{ .site.title }}: Suche &ndash; Reihen</title>

View File

@@ -295,6 +295,10 @@
@apply decoration-slate-900 line-through;
}
#searchform .searchformcolumn {
@apply grid grid-cols-12 gap-y-3 w-full gap-x-4;
}
#persontype a {
@apply px-1.5 border-b-[5px] border-transparent hover:border-zinc-200 no-underline font-serif mx-2.5;
}