mirror of
https://github.com/Theodor-Springmann-Stiftung/musenalm.git
synced 2025-10-29 09:15:33 +00:00
hot reload, search refactor begin
This commit is contained in:
@@ -8,6 +8,8 @@ full_bin = "./tmp/musenalm --dir ./pb_data serve"
|
|||||||
cmd = "go build -tags=dev,fts5,sqlite_icu -o ./tmp/musenalm ."
|
cmd = "go build -tags=dev,fts5,sqlite_icu -o ./tmp/musenalm ."
|
||||||
delay = 400
|
delay = 400
|
||||||
exclude_dir = [
|
exclude_dir = [
|
||||||
|
"views/routes",
|
||||||
|
"views/layouts",
|
||||||
"views/assets",
|
"views/assets",
|
||||||
"views/node_modules",
|
"views/node_modules",
|
||||||
"tmp",
|
"tmp",
|
||||||
@@ -32,7 +34,7 @@ log = "build-errors.log"
|
|||||||
poll = false
|
poll = false
|
||||||
poll_interval = 0
|
poll_interval = 0
|
||||||
post_cmd = []
|
post_cmd = []
|
||||||
pre_cmd = ["npm --prefix views/ run build"]
|
pre_cmd = [""]
|
||||||
rerun = false
|
rerun = false
|
||||||
rerun_delay = 250
|
rerun_delay = 250
|
||||||
send_interrupt = true
|
send_interrupt = true
|
||||||
|
|||||||
23
app/pb.go
23
app/pb.go
@@ -120,20 +120,21 @@ func (app *App) Serve() error {
|
|||||||
|
|
||||||
// INFO: hot reloading for poor people
|
// INFO: hot reloading for poor people
|
||||||
if app.MAConfig.Debug {
|
if app.MAConfig.Debug {
|
||||||
watcher, err := fsnotify.NewWatcher()
|
watcher, err := EngineWatcher(engine)
|
||||||
if err != nil {
|
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 {
|
||||||
defer watcher.Close()
|
watcher.AddRecursive(LAYOUT_DIR)
|
||||||
|
watcher.AddRecursive(ROUTES_DIR)
|
||||||
go app.watchFN(watcher, engine)
|
engine.Debug()
|
||||||
if err := watcher.Add(LAYOUT_DIR); err != nil {
|
rwatcher, err := RefreshWatcher(engine)
|
||||||
return fmt.Errorf("Failed to watch layout directory: %w.", err)
|
if err != nil {
|
||||||
|
app.PB.Logger().Error("Failed to create watcher, continuing without", "error", err)
|
||||||
|
} else {
|
||||||
|
rwatcher.Add("./views/assets")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
app.PB.OnBootstrap().BindFunc(func(e *core.BootstrapEvent) error {
|
||||||
|
|||||||
136
app/watch.go
Normal file
136
app/watch.go
Normal 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
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -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) {
|
func RContentsAgents_Agent(app core.App, id string) ([]*RContentsAgents, error) {
|
||||||
return TableByField[[]*RContentsAgents](
|
return TableByField[[]*RContentsAgents](
|
||||||
app,
|
app,
|
||||||
|
|||||||
@@ -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 {
|
func (p *AlmanachPage) Get(request *core.RequestEvent, engine *templating.Engine, data map[string]interface{}) error {
|
||||||
return engine.Response200(request, TEMPLATE_ALMANACH, data)
|
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 {
|
||||||
|
}
|
||||||
|
|||||||
247
pages/suche.go
247
pages/suche.go
@@ -1,12 +1,13 @@
|
|||||||
package pages
|
package pages
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/Theodor-Springmann-Stiftung/musenalm/app"
|
"github.com/Theodor-Springmann-Stiftung/musenalm/app"
|
||||||
"github.com/Theodor-Springmann-Stiftung/musenalm/dbmodels"
|
"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/pagemodels"
|
||||||
"github.com/Theodor-Springmann-Stiftung/musenalm/templating"
|
"github.com/Theodor-Springmann-Stiftung/musenalm/templating"
|
||||||
"github.com/pocketbase/pocketbase/core"
|
"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 {
|
router.GET(URL_SUCHE, func(e *core.RequestEvent) error {
|
||||||
if !slices.Contains(availableTypes, e.Request.PathValue("type")) {
|
paras, err := NewParameters(e)
|
||||||
return engine.Response404(e, nil, nil)
|
if err != nil {
|
||||||
|
return engine.Response404(e, err, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
q := e.Request.URL.Query().Get(PARAM_QUERY)
|
data := make(map[string]interface{})
|
||||||
q = strings.TrimSpace(q)
|
data["parameters"] = paras
|
||||||
if q != "" {
|
return engine.Response200(e, p.Template+paras.Collection+"/", data, p.Layout)
|
||||||
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)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *SuchePage) SimpleSearchRequest(app core.App, engine *templating.Engine, e *core.RequestEvent) error {
|
func (p *SuchePage) SimpleSearchReihenRequest(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)
|
|
||||||
// }
|
|
||||||
return engine.Response404(e, nil, nil)
|
return engine.Response404(e, nil, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
func (p *SuchePage) SimpleSearchBaendeRequest(app core.App, engine *templating.Engine, e *core.RequestEvent, pp Parameters) error {
|
||||||
REIHEN_PARAM_TITLE = "title"
|
data := make(map[string]interface{})
|
||||||
REIHEN_PARAM_ANNOTATIONS = "annotations"
|
params, err := NewSimpleParameters(e, pp)
|
||||||
REIHEN_PARAM_REFERENCES = "references"
|
if err != nil {
|
||||||
)
|
return engine.Response404(e, err, nil)
|
||||||
|
}
|
||||||
|
|
||||||
func (p *SuchePage) SimpleSearchReihenRequest(app core.App, engine *templating.Engine, e *core.RequestEvent) error {
|
query := dbmodels.NormalizeQuery(params.Query)
|
||||||
q := e.Request.URL.Query().Get(PARAM_QUERY)
|
if len(query) == 0 {
|
||||||
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 {
|
|
||||||
engine.Response404(e, nil, nil)
|
engine.Response404(e, nil, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
fields := []string{}
|
fields := params.FieldSetBaende()
|
||||||
options := map[string]bool{}
|
if len(fields) == 0 {
|
||||||
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 {
|
|
||||||
return engine.Response404(e, nil, nil)
|
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,
|
Fields: fields,
|
||||||
Query: query,
|
Query: query,
|
||||||
})
|
})
|
||||||
@@ -130,43 +90,20 @@ func (p *SuchePage) SimpleSearchReihenRequest(app core.App, engine *templating.E
|
|||||||
return engine.Response500(e, err, nil)
|
return engine.Response500(e, err, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
idsany := []any{}
|
idsany := datatypes.ToAny(ids)
|
||||||
for _, id := range ids {
|
entries, err := dbmodels.Entries_IDs(app, idsany)
|
||||||
idsany = append(idsany, id.ID)
|
|
||||||
}
|
|
||||||
|
|
||||||
series, err := dbmodels.SeriessesForIds(app, idsany)
|
|
||||||
rmap, bmap, err := dbmodels.EntriesForSeriesses(app, series)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return engine.Response500(e, err, nil)
|
return engine.Response500(e, err, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
dbmodels.Sort_Series_Title(series)
|
dbmodels.Sort_Entries_Title_Year(entries)
|
||||||
data["series"] = series
|
data["entries"] = entries
|
||||||
data["relations"] = rmap
|
data["count"] = len(entries)
|
||||||
data["entries"] = bmap
|
|
||||||
|
|
||||||
data["count"] = len(series)
|
eids := []any{}
|
||||||
// TODO: get relavant agents, years and places
|
for _, entry := range entries {
|
||||||
|
eids = append(eids, entry.Id)
|
||||||
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 {
|
|
||||||
|
|
||||||
return engine.Response404(e, nil, nil)
|
return engine.Response404(e, nil, nil)
|
||||||
}
|
}
|
||||||
@@ -182,19 +119,125 @@ const (
|
|||||||
// INFO: these are filter types & expanded search:
|
// INFO: these are filter types & expanded search:
|
||||||
BEITRAEGE_PARAM_TYPE = "type"
|
BEITRAEGE_PARAM_TYPE = "type"
|
||||||
BEITRAEGE_PARAM_SCANS = "scans"
|
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 {
|
var ErrInvalidCollectionType = fmt.Errorf("Invalid collection type")
|
||||||
data := p.CommonData(app, engine, e)
|
var ErrNoQuery = fmt.Errorf("No query")
|
||||||
return engine.Response200(e, p.Template, data, p.Layout)
|
|
||||||
|
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{} {
|
func NewParameters(e *core.RequestEvent) (*Parameters, error) {
|
||||||
data := map[string]interface{}{}
|
collection := e.Request.PathValue("type")
|
||||||
data["type"] = e.Request.PathValue("type")
|
if !slices.Contains(availableTypes, collection) {
|
||||||
data[PARAM_EXTENDED] = false
|
return nil, ErrInvalidCollectionType
|
||||||
if e.Request.URL.Query().Get(PARAM_EXTENDED) == "true" {
|
|
||||||
data[PARAM_EXTENDED] = true
|
|
||||||
}
|
}
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,20 +5,64 @@ import (
|
|||||||
"html/template"
|
"html/template"
|
||||||
"io"
|
"io"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/Theodor-Springmann-Stiftung/musenalm/helpers/functions"
|
"github.com/Theodor-Springmann-Stiftung/musenalm/helpers/functions"
|
||||||
"github.com/pocketbase/pocketbase/core"
|
"github.com/pocketbase/pocketbase/core"
|
||||||
|
"golang.org/x/net/websocket"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
ASSETS_URL_PREFIX = "/assets"
|
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 {
|
type Engine struct {
|
||||||
regmu *sync.Mutex
|
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
|
// NOTE: LayoutRegistry and TemplateRegistry have their own syncronization & cache and do not require a mutex here
|
||||||
LayoutRegistry *LayoutRegistry
|
LayoutRegistry *LayoutRegistry
|
||||||
@@ -44,6 +88,26 @@ func NewEngine(layouts, templates *fs.FS) *Engine {
|
|||||||
return &e
|
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 {
|
func (e *Engine) funcs() error {
|
||||||
e.mu.Lock()
|
e.mu.Lock()
|
||||||
e.mu.Unlock()
|
e.mu.Unlock()
|
||||||
@@ -113,6 +177,12 @@ func (e *Engine) Reload() {
|
|||||||
e.Load()
|
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
|
// 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{}) {
|
func (e *Engine) AddFunc(name string, fn interface{}) {
|
||||||
e.mu.Lock()
|
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 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{} {
|
func requestData(request *core.RequestEvent) map[string]interface{} {
|
||||||
|
|||||||
57
templating/ws.go
Normal file
57
templating/ws.go
Normal 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
62
views/.air.toml
Normal 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
@@ -1,5 +1,4 @@
|
|||||||
{{ $model := . }}
|
{{ $model := . }}
|
||||||
|
|
||||||
{{ if and .startpage .record }}
|
{{ if and .startpage .record }}
|
||||||
{{ template "hero" . }}
|
{{ template "hero" . }}
|
||||||
{{ end }}
|
{{ end }}
|
||||||
@@ -17,7 +16,7 @@
|
|||||||
{{ template "alphabet" Dict "active" .letter "letters" .letters "search" .search }}
|
{{ template "alphabet" Dict "active" .letter "letters" .letters "search" .search }}
|
||||||
</div>
|
</div>
|
||||||
{{ else }}
|
{{ else }}
|
||||||
<div class="mt-2 border-b border-zinc-300 w-full"></div>
|
<div class="mt-2 border-b w-full"></div>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -42,7 +41,6 @@
|
|||||||
{{ end }}
|
{{ end }}
|
||||||
</div>
|
</div>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
||||||
{{ if .series }}
|
{{ if .series }}
|
||||||
<div class="mb-1 max-w-[60rem] hyphens-auto">
|
<div class="mb-1 max-w-[60rem] hyphens-auto">
|
||||||
{{ range $id, $r := .series }}
|
{{ range $id, $r := .series }}
|
||||||
|
|||||||
61
views/routes/suche/baende/body.gohtml
Normal file
61
views/routes/suche/baende/body.gohtml
Normal 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 & 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" -}}
|
||||||
1
views/routes/suche/baende/head.gohtml
Normal file
1
views/routes/suche/baende/head.gohtml
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<title>{{ .site.title }}: Suche – Bände</title>
|
||||||
48
views/routes/suche/beitraege/body.gohtml
Normal file
48
views/routes/suche/beitraege/body.gohtml
Normal 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 & 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" -}}
|
||||||
1
views/routes/suche/beitraege/head.gohtml
Normal file
1
views/routes/suche/beitraege/head.gohtml
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<title>{{ .site.title }}: Suche – Beiträge</title>
|
||||||
17
views/routes/suche/components/_fieldscript.gohtml
Normal file
17
views/routes/suche/components/_fieldscript.gohtml
Normal 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>
|
||||||
53
views/routes/suche/components/_heading.gohtml
Normal file
53
views/routes/suche/components/_heading.gohtml
Normal 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 · <span class="">
|
||||||
|
{{- if eq $model.Collection "reihen" -}}
|
||||||
|
Reihen
|
||||||
|
{{- else if eq $model.Collection "personen" -}}
|
||||||
|
Personen & Körperschaften
|
||||||
|
{{- else if eq $model.Collection "baende" -}}
|
||||||
|
Bände
|
||||||
|
{{- else if eq $model.Collection "beitraege" -}}
|
||||||
|
Beiträge
|
||||||
|
{{- end -}}
|
||||||
|
</span>
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
{{ $model := index . 0 }}
|
{{ $parameters := index . 0 }}
|
||||||
{{ $extendable := index . 1 }}
|
{{ $extendable := index . 1 }}
|
||||||
|
{{ $q := index . 2 }}
|
||||||
|
|
||||||
|
|
||||||
<label for="q" class="hidden">Suchbegriffe</label>
|
<label for="q" class="hidden">Suchbegriffe</label>
|
||||||
<input
|
<input
|
||||||
{{ if $model.q }}value="{{ $model.q }}"{{- end -}}
|
{{ if $q }}value="{{ $q }}"{{- end -}}
|
||||||
type="search"
|
type="search"
|
||||||
name="q"
|
name="q"
|
||||||
minlength="3"
|
minlength="3"
|
||||||
@@ -16,7 +17,7 @@
|
|||||||
|
|
||||||
{{ if $extendable }}
|
{{ if $extendable }}
|
||||||
<a
|
<a
|
||||||
href="/suche/{{ $model.type }}?extended=true"
|
href="/suche/{{ $parameters.Collection }}?extended=true"
|
||||||
class="whitespace-nowrap self-end block col-span-2">
|
class="whitespace-nowrap self-end block col-span-2">
|
||||||
<i class="ri-arrow-right-long-line"></i> Erweiterte Suche
|
<i class="ri-arrow-right-long-line"></i> Erweiterte Suche
|
||||||
</a>
|
</a>
|
||||||
@@ -97,44 +97,6 @@
|
|||||||
{{- else if eq $model.type "baende" -}}
|
{{- else if eq $model.type "baende" -}}
|
||||||
<!-- INFO: Bände -->
|
<!-- INFO: Bände -->
|
||||||
{{- if not $model.extended -}}
|
{{- 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 & 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 -}}
|
{{- else -}}
|
||||||
Extended search Bände
|
Extended search Bände
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
@@ -144,28 +106,6 @@
|
|||||||
{{- if not $model.extended -}}
|
{{- if not $model.extended -}}
|
||||||
<div class="grid grid-cols-12 gap-y-3 w-full gap-x-4">
|
<div class="grid grid-cols-12 gap-y-3 w-full gap-x-4">
|
||||||
{{ template "searchboxsimple" Arr . true }}
|
{{ 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 & 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 }}
|
{{ template "infotextsimple" true }}
|
||||||
</div>
|
</div>
|
||||||
{{- else -}}
|
{{- else -}}
|
||||||
@@ -173,21 +113,3 @@
|
|||||||
{{- end -}}
|
{{- end -}}
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
</form>
|
</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>
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
<title>{{ .site.title }}: Suche</title>
|
|
||||||
1
views/routes/suche/reihen/head.gohtml
Normal file
1
views/routes/suche/reihen/head.gohtml
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<title>{{ .site.title }}: Suche – Reihen</title>
|
||||||
@@ -295,6 +295,10 @@
|
|||||||
@apply decoration-slate-900 line-through;
|
@apply decoration-slate-900 line-through;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#searchform .searchformcolumn {
|
||||||
|
@apply grid grid-cols-12 gap-y-3 w-full gap-x-4;
|
||||||
|
}
|
||||||
|
|
||||||
#persontype a {
|
#persontype a {
|
||||||
@apply px-1.5 border-b-[5px] border-transparent hover:border-zinc-200 no-underline font-serif mx-2.5;
|
@apply px-1.5 border-b-[5px] border-transparent hover:border-zinc-200 no-underline font-serif mx-2.5;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user