Lesekabinett & Startseite

This commit is contained in:
Simon Martens
2025-03-02 00:27:16 +01:00
parent 6e286857d5
commit 0a86833a9f
56 changed files with 771 additions and 445 deletions

View File

@@ -1,8 +1,6 @@
package dbmodels package dbmodels
import ( import (
"slices"
"github.com/pocketbase/dbx" "github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/core" "github.com/pocketbase/pocketbase/core"
"golang.org/x/text/collate" "golang.org/x/text/collate"
@@ -12,17 +10,6 @@ import (
type AgentsEntries map[string][]*REntriesAgents type AgentsEntries map[string][]*REntriesAgents
type AgentsContents map[string][]*RContentsAgents type AgentsContents map[string][]*RContentsAgents
func AgentForId(app core.App, id string) (*Agent, error) {
agent := &Agent{}
err := app.RecordQuery(AGENTS_TABLE).
Where(dbx.HashExp{ID_FIELD: id}).
One(agent)
if err != nil {
return nil, err
}
return agent, nil
}
func FTS5SearchAgents(app core.App, query string) ([]*Agent, error) { func FTS5SearchAgents(app core.App, query string) ([]*Agent, error) {
a := []*Agent{} a := []*Agent{}
q := NormalizeQuery(query) q := NormalizeQuery(query)
@@ -216,13 +203,6 @@ func AgentsForLetter(app core.App, letter string) ([]*Agent, error) {
return agents, nil return agents, nil
} }
func SortAgentsByName(series []*Agent) {
collator := collate.New(language.German, collate.Loose)
slices.SortFunc(series, func(i, j *Agent) int {
return collator.CompareString(i.Name(), j.Name())
})
}
func BasicSearchAgents(app core.App, query string) ([]*Agent, []*Agent, error) { func BasicSearchAgents(app core.App, query string) ([]*Agent, []*Agent, error) {
agents, err := TitleSearchAgents(app, query) agents, err := TitleSearchAgents(app, query)
if err != nil { if err != nil {
@@ -297,10 +277,11 @@ type AgentCount struct {
ID string `db:"id"` ID string `db:"id"`
} }
func CountAgentsBaende(app core.App) (map[string]int, error) { func CountAgentsBaende(app core.App, ids []any) (map[string]int, error) {
couns := []AgentCount{} couns := []AgentCount{}
err := app.RecordQuery(RelationTableName(ENTRIES_TABLE, AGENTS_TABLE)). err := app.RecordQuery(RelationTableName(ENTRIES_TABLE, AGENTS_TABLE)).
Select("count(*) as count, " + AGENTS_TABLE + " as id"). Select("count(*) as count, " + AGENTS_TABLE + " as id").
Where(dbx.HashExp{ID_FIELD: ids}).
GroupBy(AGENTS_TABLE). GroupBy(AGENTS_TABLE).
All(&couns) All(&couns)
@@ -316,7 +297,7 @@ func CountAgentsBaende(app core.App) (map[string]int, error) {
return ret, nil return ret, nil
} }
func CountAgentsContents(app core.App) (map[string]int, error) { func CountAgentsContents(app core.App, ids []any) (map[string]int, error) {
couns := []AgentCount{} couns := []AgentCount{}
err := app.RecordQuery(RelationTableName(CONTENTS_TABLE, AGENTS_TABLE)). err := app.RecordQuery(RelationTableName(CONTENTS_TABLE, AGENTS_TABLE)).
Select("count(*) as count, " + AGENTS_TABLE + " as id"). Select("count(*) as count, " + AGENTS_TABLE + " as id").

View File

@@ -3,15 +3,29 @@ package dbmodels
import ( import (
"encoding/json" "encoding/json"
"regexp" "regexp"
"sort"
"strconv" "strconv"
"strings" "strings"
) )
// CollectionInfo holds only the ID, a list of single references, and the Recorded flag. // INFO: tries to parse the Sammlungen field of contents.
// Doesn't do a good job at all, but it's hard, there are many errors
// Safe for concurrent use:
var inrex = regexp.MustCompile(`(?is)inr[.:,;]?\s*([\d,\-(?:u.?)\v\f\t –—;\.]+)`)
var onrex = regexp.MustCompile(`(?is)obj[.:,;]?\s*([\d,\-(?:u.?)\v\f\t –—;\.]+)`)
var dashRegex = regexp.MustCompile(`[–—−‒]`)
var delims = regexp.MustCompile(`[;/]+`)
var reno = regexp.MustCompile(`\b\d+\b`)
type CollectionInfo struct { type CollectionInfo struct {
Collection *Content Annotation string
Singles []int Collection int
Obj []string
INr []int
Obj_Unsure []string
INr_Unsure []int
ObjRanges []Range[string]
INrRanges []Range[int]
Recorded bool Recorded bool
} }
@@ -20,107 +34,296 @@ func (ci CollectionInfo) String() string {
return string(marshalled) return string(marshalled)
} }
// parseAnnotation detects "nicht erfasst" references (Recorded=false), func (ci CollectionInfo) ShortString() string {
// then finds all "INr" references (both single values and ranges). s := strings.Builder{}
// Ranges like "100-105" are fully expanded to singles. Duplicates are removed. s.WriteString(strconv.Itoa(ci.Collection))
// Any references not in `inos` are ignored. s.WriteString(": ")
func ParseAnnotation(c *Content, annotation string, inos []int) CollectionInfo { s.WriteString(ci.Annotation)
ci := CollectionInfo{ s.WriteString("\n")
Collection: c,
Singles: []int{}, if ci.Recorded {
Recorded: true, // Default s.WriteString("recorded")
} else {
s.WriteString("not recorded")
} }
// 1) Detect phrases like "nicht erfasst", "nicht aufgenommen", etc. s.WriteString("\n")
if len(ci.INrRanges) > 0 {
s.WriteString("INr-Ranges: ")
for _, r := range ci.INrRanges {
s.WriteString(strconv.Itoa(r.From))
s.WriteString("-")
s.WriteString(strconv.Itoa(r.To))
s.WriteString("; ")
}
s.WriteString("\n")
}
if len(ci.INr) > 0 {
s.WriteString("INr-Singles: ")
for _, i := range ci.INr {
s.WriteString(strconv.Itoa(i))
s.WriteString("; ")
}
}
if len(ci.INr_Unsure) > 0 {
s.WriteString("INr-Unsure: ")
if len(ci.INr_Unsure) > 100 {
s.WriteString("many")
} else {
for _, i := range ci.INr_Unsure {
s.WriteString(strconv.Itoa(i))
s.WriteString("; ")
}
s.WriteString("\n")
}
}
if len(ci.ObjRanges) > 0 {
s.WriteString("Obj-Ranges: ")
for _, r := range ci.ObjRanges {
s.WriteString(r.From)
s.WriteString("-")
s.WriteString(r.To)
s.WriteString("; ")
}
s.WriteString("\n")
}
if len(ci.Obj) > 0 {
s.WriteString("Obj-Singles: ")
for _, i := range ci.Obj {
s.WriteString(i)
s.WriteString("; ")
}
s.WriteString("\n")
}
if len(ci.Obj_Unsure) > 0 {
s.WriteString("Obj-Unsure: ")
for _, i := range ci.Obj_Unsure {
s.WriteString(i)
s.WriteString("; ")
}
s.WriteString("\n")
}
return s.String()
}
type Range[T any] struct {
From T
To T
}
func ParseAnnotation(c int, annotation string, inos []int, objnos []string) CollectionInfo {
ci := CollectionInfo{
Annotation: annotation,
Collection: c,
Recorded: true,
}
split := strings.Split(annotation, "/)")
inomap := make(map[int]bool)
for _, i := range inos {
inomap[i] = true
}
objnomap := make(map[string]bool)
for _, o := range objnos {
objnomap[o] = true
}
unsure_inr := func(in int) {
instr := strconv.Itoa(in)
if _, ok := objnomap[instr]; ok {
ci.Obj = append(ci.Obj, instr)
} else {
ci.INr_Unsure = append(ci.INr_Unsure, in)
}
}
unsure_inr_range := func(r Range[int]) {
cfrom := strconv.Itoa(r.From)
cto := strconv.Itoa(r.To)
_, ok := objnomap[cfrom]
_, ok2 := objnomap[cto]
if ok && ok2 {
ci.ObjRanges = append(ci.ObjRanges, Range[string]{From: cfrom, To: cto})
} else {
for i := r.From; i <= r.To; i++ {
unsure_inr(i)
}
}
}
for _, s := range split {
l := strings.ToLower(s)
// TODO: before this, we may cut the annotation into /) pieces
notRecordedPatterns := []string{"erfasst", "aufgenommen", "verzeichnet", "registriert"} notRecordedPatterns := []string{"erfasst", "aufgenommen", "verzeichnet", "registriert"}
lowerAnn := strings.ToLower(annotation) if strings.Contains(l, "nicht") {
if strings.Contains(lowerAnn, "nicht") {
for _, kw := range notRecordedPatterns { for _, kw := range notRecordedPatterns {
if strings.Contains(lowerAnn, kw) { if strings.Contains(l, kw) {
ci.Recorded = false ci.Recorded = false
break break
} }
} }
} }
// We'll keep singles in a map for deduplication matches := inrex.FindAllStringSubmatch(s, -1)
singlesMap := make(map[int]struct{}) inRanges, inSingles := findINrRangesSingles(matches)
// 2) Regex that matches "INr" plus the numeric portion (including dash / punctuation). // INFO: Heuristics
re := regexp.MustCompile(`(?i)\bINr[.:]?\s+([\d,\-\s—;/.]+)`) for _, in := range inSingles {
matches := re.FindAllStringSubmatch(annotation, -1) if _, ok := inomap[in]; ok {
ci.INr = append(ci.INr, in)
// Regex to unify different dash characters into a simple '-' } else {
dashRegex := regexp.MustCompile(`[–—−‒]`) unsure_inr(in)
// Helper to expand a range, e.g. 1061510621 => 10615..10621
expandRange := func(fromVal, toVal int) {
// If reversed, its a typo
if fromVal > toVal {
return
} }
for v := fromVal; v <= toVal; v++ { }
if inList(v, inos) {
singlesMap[v] = struct{}{} for _, r := range inRanges {
if r.From < r.To {
_, ok := inomap[r.From]
_, ok2 := inomap[r.To]
if ok && ok2 {
ci.INrRanges = append(ci.INrRanges, r)
continue
}
unsure_inr_range(r)
} else {
for i := r.From; i <= r.To; i++ {
ci.INr_Unsure = append(ci.INr_Unsure, i)
} }
} }
} }
for _, m := range matches { matches = onrex.FindAllStringSubmatch(s, -1)
numericChunk := m[1] objRanges, objSingles := findONrRangesSingles(matches)
// Replace typographic dashes with ASCII hyphen for _, o := range objSingles {
numericChunk = dashRegex.ReplaceAllString(numericChunk, "-") if _, ok := objnomap[o]; ok {
ci.Obj = append(ci.Obj, o)
} else {
ci.Obj_Unsure = append(ci.Obj_Unsure, o)
}
}
// Also unify semicolons or slashes to commas for _, r := range objRanges {
extraDelims := regexp.MustCompile(`[;/]+`) if r.From < r.To {
numericChunk = extraDelims.ReplaceAllString(numericChunk, ",") _, ok := objnomap[r.From]
_, ok2 := objnomap[r.To]
if ok && ok2 {
ci.ObjRanges = append(ci.ObjRanges, r)
continue
}
}
}
// Now split on commas }
parts := strings.Split(numericChunk, ",")
return ci
}
func findINrRangesSingles(matches [][]string) ([]Range[int], []int) {
ranges := make([]Range[int], 0)
singles := make([]int, 0)
for _, match := range matches {
chunk := match[1]
normalized := dashRegex.ReplaceAllString(chunk, "-")
// WARNING: Replacing the OBj and INr delimiter ; with a comma here.
// It's a problem if the Obj was left out: INr 323345-323398; 23-53
// Here is an Obj often, but not always ^
// We do some heuristics later on to differentiate INr from Obj.
normalized = delims.ReplaceAllString(normalized, ",")
parts := strings.Split(normalized, ",")
for _, p := range parts { for _, p := range parts {
p = strings.TrimSpace(p) p = strings.TrimSpace(p)
if p == "" { if p == "" {
continue continue
} }
// If we see a hyphen, treat it as a range
if strings.Contains(p, "-") { rangeParts := strings.Split(p, "-")
rangeParts := strings.SplitN(p, "-", 2)
if len(rangeParts) == 2 { if len(rangeParts) == 2 {
// INFO: we have a range, most prob
fromStr := strings.TrimSpace(rangeParts[0]) fromStr := strings.TrimSpace(rangeParts[0])
toStr := strings.TrimSpace(rangeParts[1]) toStr := strings.TrimSpace(rangeParts[1])
if fromVal, errFrom := strconv.Atoi(fromStr); errFrom == nil { if fromVal, errFrom := strconv.Atoi(fromStr); errFrom == nil {
if toVal, errTo := strconv.Atoi(toStr); errTo == nil { if toVal, errTo := strconv.Atoi(toStr); errTo == nil {
expandRange(fromVal, toVal) ranges = append(ranges, Range[int]{From: fromVal, To: toVal})
continue
}
to := reno.FindAllString(toStr, -1)
if len(to) >= 1 {
if val, err := strconv.Atoi(to[0]); err == nil {
ranges = append(ranges, Range[int]{From: fromVal, To: val})
}
if len(to) > 1 {
if val, err := strconv.Atoi(to[1]); err == nil {
singles = append(singles, val)
} }
} }
} }
} else { }
// Single integer reference continue
if val, err := strconv.Atoi(p); err == nil { }
if inList(val, inos) {
singlesMap[val] = struct{}{} rangeParts = strings.Split(p, " u")
for _, r := range rangeParts {
trimmed := strings.TrimSpace(r)
matches := reno.FindAllString(trimmed, -1)
for _, m := range matches {
if val, err := strconv.Atoi(m); err == nil {
singles = append(singles, val)
} }
} }
} }
} }
} }
// Flatten the map into a sorted slice return ranges, singles
for s := range singlesMap {
ci.Singles = append(ci.Singles, s)
}
sort.Ints(ci.Singles)
return ci
} }
// inList checks membership in `inos` func findONrRangesSingles(matches [][]string) ([]Range[string], []string) {
func inList(x int, list []int) bool { ranges := make([]Range[string], 0)
for _, item := range list { singles := make([]string, 0)
if item == x {
return true for _, match := range matches {
chunk := match[1]
normalized := dashRegex.ReplaceAllString(chunk, "-")
normalized = delims.ReplaceAllString(normalized, ",")
parts := strings.Split(normalized, ",")
for _, p := range parts {
p = strings.TrimSpace(p)
if p == "" {
continue
}
rangeParts := strings.Split(p, "-")
if len(rangeParts) == 2 {
// INFO: we have a range, most prob
fromStr := strings.TrimSpace(rangeParts[0])
toStr := strings.TrimSpace(rangeParts[1])
ranges = append(ranges, Range[string]{From: fromStr, To: toStr})
continue
}
rangeParts = strings.Split(p, " u")
for _, r := range rangeParts {
trimmed := strings.TrimSpace(r)
matches := reno.FindAllString(trimmed, -1)
for _, m := range matches {
singles = append(singles, m)
} }
} }
return false }
}
return ranges, singles
} }

View File

@@ -1,5 +0,0 @@
package dbmodels
type FieldMetaData struct {
MetaData MetaData `json:",omitempty" db:"edit_fielddata"`
}

View File

@@ -1,86 +0,0 @@
package dbmodels
import (
"slices"
"strings"
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/core"
)
type ContentsAgents map[string][]*RContentsAgents
func ContentsForEntry(app core.App, entry *Entry) ([]*Content, error) {
contents := []*Content{}
err := app.RecordQuery(CONTENTS_TABLE).
Where(dbx.HashExp{ENTRIES_TABLE: entry.Id}).
All(&contents)
if err != nil {
return nil, err
}
slices.SortFunc(contents, func(i, j *Content) int {
r := i.Numbering() - j.Numbering()
if r == 0 {
return 0
}
if r < 0 {
return -1
}
return 1
})
return contents, nil
}
func ContentsForAgent(app core.App, agentId string) ([]*Content, error) {
relations := []*RContentsAgents{}
err := app.RecordQuery(RelationTableName(CONTENTS_TABLE, AGENTS_TABLE)).
Where(dbx.HashExp{AGENTS_TABLE: agentId}).
All(&relations)
if err != nil {
return nil, err
}
cids := []any{}
for _, r := range relations {
cids = append(cids, r.Content())
}
contents := []*Content{}
err = app.RecordQuery(CONTENTS_TABLE).
Where(dbx.HashExp{ID_FIELD: cids}).
All(&contents)
if err != nil {
return nil, err
}
return contents, nil
}
func SortContentsByEntryNumbering(contents []*Content, entries map[string]*Entry) {
slices.SortFunc(contents, func(i, j *Content) int {
ii, iok := entries[i.Entry()]
ij, jok := entries[j.Entry()]
if iok && jok {
ret := ii.Year() - ij.Year()
if ret != 0 {
return ret
}
ret = strings.Compare(ii.PreferredTitle(), ij.PreferredTitle())
if ret != 0 {
return ret
}
}
r := i.Numbering() - j.Numbering()
if r == 0 {
return 0
}
if r < 0 {
return -1
}
return 1
})
}

View File

@@ -1,66 +0,0 @@
package dbmodels
import (
"slices"
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/core"
"golang.org/x/text/collate"
"golang.org/x/text/language"
)
func AllPlaces(app core.App) ([]*Place, error) {
places := []*Place{}
err := app.RecordQuery(PLACES_TABLE).
OrderBy(PLACES_NAME_FIELD).
All(&places)
if err != nil {
return nil, err
}
return places, nil
}
func SortPlacesByName(places []*Place) {
collator := collate.New(language.German)
slices.SortFunc(places, func(i, j *Place) int {
return collator.CompareString(i.Name(), j.Name())
})
}
func PlaceForId(app core.App, id string) (*Place, error) {
place := &Place{}
err := app.RecordQuery(PLACES_TABLE).
Where(dbx.HashExp{ID_FIELD: id}).
One(place)
if err != nil {
return nil, err
}
return place, nil
}
func PlacesForEntry(app core.App, entry *Entry) (map[string]*Place, error) {
ids := []any{}
places := map[string]*Place{}
for _, r := range entry.Places() {
ids = append(ids, r)
}
if len(ids) == 0 {
return places, nil
}
p := []*Place{}
err := app.RecordQuery(PLACES_TABLE).
Where(dbx.HashExp{ID_FIELD: ids}).
All(&p)
if err != nil {
return nil, err
}
for _, place := range p {
places[place.Id] = place
}
return places, nil
}

View File

@@ -12,6 +12,8 @@ import (
// - any id or multiple IDs (of an indexed field) // - any id or multiple IDs (of an indexed field)
// 3. Naming convention: <TableName>_<FilteredField>[s] // 3. Naming convention: <TableName>_<FilteredField>[s]
// For scanning, with an Iter_ prefix, yields single row results // For scanning, with an Iter_ prefix, yields single row results
// TODO: It would be nice if the return types of these, if arrays were custom types that implemented
// some often uses functions, like getting all IDs, or creating a map of the IDs.
func REntriesAgents_Agent(app core.App, id string) ([]*REntriesAgents, error) { func REntriesAgents_Agent(app core.App, id string) ([]*REntriesAgents, error) {
return TableByFields[*REntriesAgents]( return TableByFields[*REntriesAgents](

View File

@@ -1,10 +1,6 @@
package dbmodels package dbmodels
import ( import ()
"iter"
"github.com/pocketbase/pocketbase/core"
)
// INFO: Iterator queries to be reused // INFO: Iterator queries to be reused
// Rules // Rules
@@ -15,23 +11,24 @@ import (
// 3. Naming convention: Iter_<TableName>_<FilteredField>[s] // 3. Naming convention: Iter_<TableName>_<FilteredField>[s]
// BUG: this is not working as expected, see Iter_TableByField in queryhelpers.go // BUG: this is not working as expected, see Iter_TableByField in queryhelpers.go
func Iter_REntriesAgents_Agent(app core.App, id string) (iter.Seq2[*REntriesAgents, error], error) {
innerIterator, err := Iter_TableByField[REntriesAgents](
app,
RelationTableName(ENTRIES_TABLE, AGENTS_TABLE),
AGENTS_TABLE,
id,
)
if err != nil { // func Iter_REntriesAgents_Agent(app core.App, id string) (iter.Seq2[*REntriesAgents, error], error) {
return nil, err // innerIterator, err := Iter_TableByField[REntriesAgents](
} // app,
// RelationTableName(ENTRIES_TABLE, AGENTS_TABLE),
return func(yield func(*REntriesAgents, error) bool) { // AGENTS_TABLE,
for item, err := range innerIterator { // id,
if !yield(item, err) { // )
return //
} // if err != nil {
} // return nil, err
}, nil // }
} //
// return func(yield func(*REntriesAgents, error) bool) {
// for item, err := range innerIterator {
// if !yield(item, err) {
// return
// }
// }
// }, nil
// }

View File

@@ -1,7 +1,6 @@
package dbmodels package dbmodels
import ( import (
"iter"
"reflect" "reflect"
"github.com/pocketbase/dbx" "github.com/pocketbase/dbx"
@@ -10,100 +9,50 @@ import (
// INFO: These functions are very abstract interfaces to the DB that help w querying // INFO: These functions are very abstract interfaces to the DB that help w querying
// BUG: this is not working:
// github.com/pocketbase/pocketbase/apis.NewRouter.panicRecover.func3.1()
//
// /home/simon/go/pkg/mod/github.com/pocketbase/pocketbase@v0.25.5/apis/middlewares.go:269 +0x13c
//
// panic({0x15b34c0?, 0x2831680?})
//
// /usr/local/go/src/runtime/panic.go:787 +0x132
//
// github.com/pocketbase/pocketbase/core.(*Record).FieldsData(0xc000632820)
//
// /home/simon/go/pkg/mod/github.com/pocketbase/pocketbase@v0.25.5/core/record_model.go:774 +0x1a
//
// github.com/pocketbase/pocketbase/core.(*Record).PostScan(0xc000632820)
//
// /home/simon/go/pkg/mod/github.com/pocketbase/pocketbase@v0.25.5/core/record_model.go:591 +0x4e
//
// github.com/pocketbase/dbx.(*Rows).ScanStruct(0xc00052e6d0, {0x175f840?, 0xc000586060?})
//
// /home/simon/go/pkg/mod/github.com/pocketbase/dbx@v1.11.0/rows.go:97 +0x32e
//
// github.com/Theodor-Springmann-Stiftung/musenalm/dbmodels.Iter_TableByField[...].func1()
//
// /home/simon/source/musenalm/dbmodels/queryhelpers.go:23 +0x65
//
// github.com/Theodor-Springmann-Stiftung/musenalm/dbmodels.Iter_REntriesAgents_Agent.func1(0xc000624840)
//
// /home/simon/source/musenalm/dbmodels/queries_iter.go:30 +0xae
//
// github.com/Theodor-Springmann-Stiftung/musenalm/pages.(*PersonResult).FilterEntriesByPerson(0x1762c40?, {0x1dfba88, 0xc000438870}, {0xc00004627c, 0xf}, 0xc000064720)
//
// /home/simon/source/musenalm/pages/person.go:111 +0x248
//
// github.com/Theodor-Springmann-Stiftung/musenalm/pages.NewPersonResult({0x1dfba88, 0xc000438870}, {0xc00004627c, 0xf})
//
// /home/simon/source/musenalm/pages/person.go:92 +0x4f
//
// github.com/Theodor-Springmann-Stiftung/musenalm/pages.(*PersonPage).Setup.func1(0xc0002da000)
//
// /home/simon/source/musenalm/pages/person.go:46 +0x1ee
//
// github.com/pocketbase/pocketbase/tools/hook.(*Hook[...]).Trigger.func1()
//
// /home/simon/go/pkg/mod/github.com/pocketbase/pocketbase@v0.25.5/tools/hook/hook.go:169 +0x5d
//
// github.com/pocketbase/pocketbase/tools/hook.(*Event).Next(0xc0002da000?)
//
// /home/simon/go/pkg/mod/github.com/pocketbase/pocketbase@v0.25.5/tools/hook/event.go:32 +0x17
//
// github.com/pocketbase/pocketbase/apis.NewRouter.BodyLimit.func7(0xc0002da000)
//
// /home
const ( const (
QUERY_PARTITION_SIZE = 1200 QUERY_PARTITION_SIZE = 1200
) )
func Iter_TableByField[T interface{}](app core.App, table, field string, value interface{}) (iter.Seq2[*T, error], error) { // BUG: this is not working, throws an exception
rows, err := app.RecordQuery(table). // github.com/pocketbase/pocketbase/apis.NewRouter.panicRecover.func3.1()
Where(dbx.HashExp{field: value}). //
Rows() // func Iter_TableByField[T interface{}](app core.App, table, field string, value interface{}) (iter.Seq2[*T, error], error) {
if err != nil { // rows, err := app.RecordQuery(table).
return nil, err // Where(dbx.HashExp{field: value}).
} // Rows()
// if err != nil {
return func(yield func(*T, error) bool) { // return nil, err
for rows.Next() { // }
var item T //
err := rows.ScanStruct(&item) // return func(yield func(*T, error) bool) {
if !yield(&item, err) { // for rows.Next() {
return // var item T
} // err := rows.ScanStruct(&item)
} // if !yield(&item, err) {
}, nil // return
} // }
// }
func Iter_TableByID[T interface{}](app core.App, table, id interface{}) (iter.Seq2[*T, error], error) { // }, nil
rows, err := app.RecordQuery(table). // }
Where(dbx.HashExp{ID_FIELD: id}). //
Rows() // func Iter_TableByID[T interface{}](app core.App, table, id interface{}) (iter.Seq2[*T, error], error) {
if err != nil { // rows, err := app.RecordQuery(table).
return nil, err // Where(dbx.HashExp{ID_FIELD: id}).
} // Rows()
// if err != nil {
return func(yield func(*T, error) bool) { // return nil, err
for rows.Next() { // }
var item T //
rows.Scan(&item) // return func(yield func(*T, error) bool) {
if !yield(&item, nil) { // for rows.Next() {
return // var item T
} // rows.Scan(&item)
} // if !yield(&item, nil) {
}, nil // return
} // }
// }
// }, nil
// }
func TableByField[T interface{}](app core.App, table, field string, value string) (T, error) { func TableByField[T interface{}](app core.App, table, field string, value string) (T, error) {
var ret T var ret T

View File

@@ -8,6 +8,8 @@ import (
"golang.org/x/text/language" "golang.org/x/text/language"
) )
// INFO: Functions that implement sorting of which sqlite is not capable of.
func Sort_Series_Title(series []*Series) { func Sort_Series_Title(series []*Series) {
collator := collate.New(language.German) collator := collate.New(language.German)
slices.SortFunc(series, func(i, j *Series) int { slices.SortFunc(series, func(i, j *Series) int {
@@ -55,3 +57,10 @@ func Sort_Contents_Numbering(contents []*Content) {
return datatypes.CompareFloat(i.Numbering(), j.Numbering()) return datatypes.CompareFloat(i.Numbering(), j.Numbering())
}) })
} }
func Sort_Places_Name(places []*Place) {
collator := collate.New(language.German)
slices.SortFunc(places, func(i, j *Place) int {
return collator.CompareString(i.Name(), j.Name())
})
}

View File

@@ -12,6 +12,7 @@ import (
_ "github.com/Theodor-Springmann-Stiftung/musenalm/pages/migrations_einfuehrung" _ "github.com/Theodor-Springmann-Stiftung/musenalm/pages/migrations_einfuehrung"
_ "github.com/Theodor-Springmann-Stiftung/musenalm/pages/migrations_index" _ "github.com/Theodor-Springmann-Stiftung/musenalm/pages/migrations_index"
_ "github.com/Theodor-Springmann-Stiftung/musenalm/pages/migrations_kontakt" _ "github.com/Theodor-Springmann-Stiftung/musenalm/pages/migrations_kontakt"
_ "github.com/Theodor-Springmann-Stiftung/musenalm/pages/migrations_lesekabinett"
_ "github.com/Theodor-Springmann-Stiftung/musenalm/pages/migrations_literatur" _ "github.com/Theodor-Springmann-Stiftung/musenalm/pages/migrations_literatur"
_ "github.com/Theodor-Springmann-Stiftung/musenalm/pages/migrations_reihen" _ "github.com/Theodor-Springmann-Stiftung/musenalm/pages/migrations_reihen"
"github.com/pocketbase/pocketbase/plugins/migratecmd" "github.com/pocketbase/pocketbase/plugins/migratecmd"

View File

@@ -43,6 +43,12 @@ func (b *IndexBilder) SetBild(bild *filesystem.File) {
b.Set(F_IMAGE, bild) b.Set(F_IMAGE, bild)
} }
func (r *IndexBilder) BildPath() string {
img := r.Bild()
ret := "/api/files/" + r.TableName() + "/" + r.Id + "/" + img
return ret
}
func (b *IndexBilder) Vorschau() string { func (b *IndexBilder) Vorschau() string {
return b.GetString(F_PREVIEW) return b.GetString(F_PREVIEW)
} }
@@ -51,6 +57,12 @@ func (b *IndexBilder) SetVorschau(vorschau *filesystem.File) {
b.Set(F_PREVIEW, vorschau) b.Set(F_PREVIEW, vorschau)
} }
func (r *IndexBilder) VorschauPath() string {
img := r.Vorschau()
ret := "/api/files/" + r.TableName() + "/" + r.Id + "/" + img
return ret
}
type IndexTexte struct { type IndexTexte struct {
core.BaseRecordProxy core.BaseRecordProxy
} }

View File

@@ -1,6 +1,7 @@
package pagemodels package pagemodels
const ( const (
P_KABINETT_NAME = "lesekabinett"
P_BEITRAG_NAME = "beitrag" P_BEITRAG_NAME = "beitrag"
P_DATENSCHUTZ_NAME = "datenschutz" P_DATENSCHUTZ_NAME = "datenschutz"

View File

@@ -159,16 +159,4 @@ func (r *AlmanachResult) Collections() {
} }
} }
ccontentcollectionmap := map[int][]dbmodels.CollectionInfo{}
ccollectioncontentmap := map[string]dbmodels.CollectionInfo{}
for _, v := range collections {
cinfo := dbmodels.ParseAnnotation(v, v.Annotation(), ids)
ccollectioncontentmap[v.Id] = cinfo
for _, c := range cinfo.Singles {
ccontentcollectionmap[c] = append(ccontentcollectionmap[c], cinfo)
}
}
r.CInfoByCollection = ccollectioncontentmap
r.CInfoByContent = ccontentcollectionmap
} }

View File

@@ -1,14 +1,14 @@
package pages package pages
import ( import (
"net/http" "time"
"strings"
"github.com/Theodor-Springmann-Stiftung/musenalm/app" "github.com/Theodor-Springmann-Stiftung/musenalm/app"
"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"
"github.com/pocketbase/pocketbase/tools/router" "github.com/pocketbase/pocketbase/tools/router"
"math/rand"
) )
func init() { func init() {
@@ -27,12 +27,36 @@ type IndexPage struct {
// TODO: // TODO:
func (p *IndexPage) Setup(router *router.Router[*core.RequestEvent], app core.App, engine *templating.Engine) error { func (p *IndexPage) Setup(router *router.Router[*core.RequestEvent], app core.App, engine *templating.Engine) error {
router.GET("/{$}", func(e *core.RequestEvent) error { router.GET("/{$}", func(e *core.RequestEvent) error {
var builder strings.Builder bilder := []*pagemodels.IndexBilder{}
err := engine.Render(&builder, "/", nil, "blank") err := app.RecordQuery(pagemodels.GeneratePageTableName(pagemodels.P_INDEX_NAME, pagemodels.T_INDEX_BILDER)).
All(&bilder)
if err != nil { if err != nil {
return err return engine.Response404(e, err, nil)
} }
return e.HTML(http.StatusOK, builder.String()) texte := []*pagemodels.IndexTexte{}
err = app.RecordQuery(pagemodels.GeneratePageTableName(pagemodels.P_INDEX_NAME)).
All(&texte)
if err != nil {
return engine.Response404(e, err, nil)
}
Shuffle(bilder)
data := map[string]interface{}{
"bilder": bilder,
"texte": texte[0],
}
return engine.Response200(e, "/", data, "blank")
}) })
return nil return nil
} }
func Shuffle[T any](arr []T) {
rand.Seed(time.Now().UnixNano()) // Ensure random seed
n := len(arr)
for i := n - 1; i > 0; i-- {
j := rand.Intn(i + 1) // Get a random index
arr[i], arr[j] = arr[j], arr[i] // Swap
}
}

View File

@@ -0,0 +1,63 @@
package migrations_index
import (
"github.com/Theodor-Springmann-Stiftung/musenalm/pagemodels"
"github.com/pocketbase/pocketbase/core"
m "github.com/pocketbase/pocketbase/migrations"
)
var text = `<h1>Texte zum Almanachwesen</h1>
<p><em>Joseph Franz von Ratschky:</em> Vorbericht. in: Wiener Musenalmanach. 1779, S. 3-6. [&darr;<a href="/assets/Lesekabinett/ratschky_in_wiener_1779.pdf" target="_blank" rel="noopener">Download</a>]</p>
<p><em>Gottfried August B&uuml;rger:</em> Nothgedrungene Nachrede. in: G&ouml;ttinger Musenalmanach. 1782, S. 184-192. [&darr;<a href="/assets/Lesekabinett/buerger_in_goettinger_1782.pdf" target="_blank" rel="noopener">Download</a>]</p>
<p><em>Christian Cay Lorenz Hirschfeld: </em>An die Leser. in: Gartenkalender. 1783, S. 272. [&darr;<a href="/assets/Lesekabinett/hirschfeld_in_gartenkalender_1783.pdf" target="_blank" rel="noopener">Download</a>]</p>
<p><em>Johann Heinrich Vo&szlig;:</em> Ank&uuml;ndigung. in: Hamburger Musenalmanach. 1784, S. 222ff. [&darr;<a href="/assets/Lesekabinett/voss_in_hamburger_1784.pdf" target="_blank" rel="noopener">Download</a>]</p>
<p><em>Gotthold Friedrich St&auml;udlin: </em>Nachrede. in: Schw&auml;bischer Musenalmanach. 1786 [o. S.]. [&darr;<a href="/assets/Lesekabinett/staeudlin_in_schwaebischer_1786.pdf" target="_blank" rel="noopener">Download</a>]</p>
<p><em>Gottfried August B&uuml;rger:</em> F&uuml;rbitte eines ans peinliche Kreuz der Verlegenheit genagelten Herausgebers eines Musenalmanachs. in: G&ouml;ttinger Musenalmanach. 1789, S. 104. [&darr;<a href="/assets/Lesekabinett/buerger_in_goettinger_1789.pdf" target="_blank" rel="noopener">Download</a>]</p>
<p><em>Anonymus: </em>Die deutschen Almanache. in: Bibliothek der redenden und bildenden K&uuml;nste. Zweyten Bandes erstes St&uuml;ck. Leipzig, in der Dyckischen Buchhandlung, 1806, S. 207-217. [&darr;<a href="/assets/Lesekabinett/anonymus.pdf" target="_blank" rel="noopener">Download</a>]</p>
<p><em>Stephan Sch&uuml;tze:</em> Die Neujahrsversammlung. Ein dramatischer Prolog. in: Taschenbuch der Liebe und Freundschaft gewidmet. 1813, S. 1-20. [&darr;<a href="/assets/Lesekabinett/schuetze_in_taschenbuch_1813.pdf" target="_blank" rel="noopener">Download</a>]</p>
<p><em>N. B. E.: </em>Die deutschen Taschenb&uuml;cher f&uuml;r 1820. in: Hermes oder kritisches Jahrbuch der Literatur. Zweites St&uuml;ck f&uuml;r das Jahr 1820. Amsterdam, in der Verlags-Expedition des Hermes, S. 191-235. [&darr;<em><a href="/assets/Lesekabinett/nbe_in_hermes_1820.pdf" target="_blank" rel="noopener">Download</a>]</em></p>
<p><em>Ferdinand Johannes Wit:</em> Die Almanachomanie. in: Politisches Taschenbuch. 1831, S. 102-111. [&darr;<a href="/assets/Lesekabinett/wit_in_politaschenbuch_1831.pdf" target="_blank" rel="noopener">Download</a>]</p>
<p><em>August Wilhelm Schlegel:</em> Recept. in: Deutscher Musenalmanach (Chamisso, Schwab, Gaudy). 1836, S. 18. [&darr;<a href="/assets/Lesekabinett/schlegel_in_deutscher_1836.pdf" target="_blank" rel="noopener">Download</a>]</p>
<p><em>Robert Eduard Prutz:</em> Die Musenalmanache und Taschenb&uuml;cher in Deutschland. in: Neue Schriften. Zur deutschen Literatur- und Kulturgeschichte. Erster Band, Halle, G. Schwetschke'scher Verlag, 1854, S. 105-165. [&darr;<a href="/assets/Lesekabinett/prutz_in_musenalmanache_1854.pdf" target="_blank" rel="noopener">Download</a>]</p>
<h1>Allotria und Kuriosa</h1>
<p><em>Anonymus:</em> Woher das Wort Almanach komme. in: Neues Wochenblatt zum Nuzzen und zur Unterhaltung f&uuml;r Kinder und junge Leute. Erstes B&auml;ndchen, erstes St&uuml;ck, Leipzig, in der Sommerschen Buchhandlung 1794, S. 8f. [&darr;<a href="/assets/Lesekabinett/allatroia_anonymus_wochenblatt_1794.pdf" target="_blank" rel="noopener">Download</a>]</p>`
var texte_fields = core.NewFieldsList(
pagemodels.EditorField(pagemodels.F_TEXT),
)
func init() {
m.Register(func(app core.App) error {
collection_t := texteCollection()
if err := app.Save(collection_t); err != nil {
return err
}
r := core.NewRecord(collection_t)
page := pagemodels.NewTextPage(r)
page.SetText(text)
page.SetTitle("Lesekabinett")
if err := app.Save(r); err != nil {
return err
}
return nil
}, func(app core.App) error {
collection_t, err := app.FindCollectionByNameOrId(
pagemodels.GeneratePageTableName(pagemodels.P_KABINETT_NAME))
if err == nil && collection_t != nil {
if err := app.Delete(collection_t); err != nil {
return err
}
}
return nil
})
}
func texteCollection() *core.Collection {
c := pagemodels.BasePageCollection(pagemodels.P_KABINETT_NAME)
c.Fields = append(c.Fields, texte_fields...)
return c
}

View File

@@ -9,6 +9,7 @@ import (
"github.com/pocketbase/pocketbase/tools/router" "github.com/pocketbase/pocketbase/tools/router"
) )
// INFO: V0 of these
const ( const (
URL_PERSONEN = "/personen/" URL_PERSONEN = "/personen/"
PARAM_FILTER = "filter" PARAM_FILTER = "filter"
@@ -86,17 +87,22 @@ func (p *PersonenPage) FilterRequest(app core.App, engine *templating.Engine, e
if err != nil { if err != nil {
return engine.Response404(e, err, data) return engine.Response404(e, err, data)
} }
dbmodels.SortAgentsByName(agents) dbmodels.Sort_Agents_Name(agents)
data["agents"] = agents data["agents"] = agents
data["filter"] = filter data["filter"] = filter
data["letter"] = letter data["letter"] = letter
bcount, err := dbmodels.CountAgentsBaende(app) ids := []any{}
for _, a := range agents {
ids = append(ids, a.Id)
}
bcount, err := dbmodels.CountAgentsBaende(app, ids)
if err == nil { if err == nil {
data["bcount"] = bcount data["bcount"] = bcount
} }
count, err := dbmodels.CountAgentsContents(app) count, err := dbmodels.CountAgentsContents(app, ids)
if err == nil { if err == nil {
data["ccount"] = count data["ccount"] = count
} }
@@ -135,19 +141,28 @@ func (p *PersonenPage) SearchRequest(app core.App, engine *templating.Engine, e
data["FTS"] = true data["FTS"] = true
} }
dbmodels.SortAgentsByName(agents) dbmodels.Sort_Agents_Name(agents)
dbmodels.SortAgentsByName(altagents) dbmodels.Sort_Agents_Name(altagents)
data["search"] = search data["search"] = search
data["agents"] = agents data["agents"] = agents
data["altagents"] = altagents data["altagents"] = altagents
bcount, err := dbmodels.CountAgentsBaende(app) ids := []any{}
for _, a := range agents {
ids = append(ids, a.Id)
}
for _, a := range altagents {
ids = append(ids, a.Id)
}
bcount, err := dbmodels.CountAgentsBaende(app, ids)
if err == nil { if err == nil {
data["bcount"] = bcount data["bcount"] = bcount
} }
count, err := dbmodels.CountAgentsContents(app) count, err := dbmodels.CountAgentsContents(app, ids)
if err == nil { if err == nil {
data["ccount"] = count data["ccount"] = count
} }

View File

@@ -208,7 +208,7 @@ func NewCommonReihenData(app core.App) CommonReihenData {
if err != nil { if err != nil {
app.Logger().Error("Failed to fetch places", "error", err) app.Logger().Error("Failed to fetch places", "error", err)
} }
dbmodels.SortPlacesByName(places) dbmodels.Sort_Places_Name(places)
rec := []core.Record{} rec := []core.Record{}
err = app.RecordQuery(dbmodels.ENTRIES_TABLE). err = app.RecordQuery(dbmodels.ENTRIES_TABLE).

View File

@@ -8,11 +8,12 @@ import (
func init() { func init() {
RegisterStaticPage("/datenschutz/", pagemodels.P_DATENSCHUTZ_NAME) RegisterStaticPage("/datenschutz/", pagemodels.P_DATENSCHUTZ_NAME)
RegisterTextPage("/edition/kontakt/", pagemodels.P_KONTAKT_NAME) RegisterTextPage("/redaktion/kontakt/", pagemodels.P_KONTAKT_NAME)
RegisterTextPage("/edition/danksagungen/", pagemodels.P_DANK_NAME) RegisterTextPage("/redaktion/danksagungen/", pagemodels.P_DANK_NAME)
RegisterTextPage("/edition/literatur/", pagemodels.P_LIT_NAME) RegisterTextPage("/redaktion/literatur/", pagemodels.P_LIT_NAME)
RegisterTextPage("/edition/einfuehrung/", pagemodels.P_EINFUEHRUNG_NAME) RegisterTextPage("/redaktion/einfuehrung/", pagemodels.P_EINFUEHRUNG_NAME)
RegisterTextPage("/edition/dokumentation/", pagemodels.P_DOK_NAME) RegisterTextPage("/redaktion/dokumentation/", pagemodels.P_DOK_NAME)
RegisterTextPage("/redaktion/lesekabinett/", pagemodels.P_KABINETT_NAME)
} }
func RegisterStaticPage(url, name string) { func RegisterStaticPage(url, name string) {

View File

@@ -38,6 +38,14 @@
src: url(/assets/fonts/SourceSans3-BoldItalic.ttf) format("truetype"); src: url(/assets/fonts/SourceSans3-BoldItalic.ttf) format("truetype");
} }
@font-face {
font-family: "Spectral";
font-style: normal;
font-weight: 500;
font-display: swap;
src: url(/assets/fonts/Spectral-Regular.ttf) format("truetype");
}
@font-face { @font-face {
font-family: "Source Sans 3"; font-family: "Source Sans 3";
font-style: normal; font-style: normal;

View File

@@ -2,6 +2,9 @@
<html class="w-full h-full" {{ if .lang }}lang="{{ .lang }}"{{ end }}> <html class="w-full h-full" {{ if .lang }}lang="{{ .lang }}"{{ end }}>
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta
name="htmx-config"
content='{"defaultSwapStyle":"outerHTML", "scrollBehavior": "instant"}' />
{{ block "head" . }} {{ block "head" . }}
<!-- Default Head elements --> <!-- Default Head elements -->
@@ -9,27 +12,38 @@
{{ if .isDev }} {{ if .isDev }}
<link rel="icon" href="/assets/logo/dev_favicon.png" /> <link rel="icon" href="/assets/logo/dev_favicon.png" />
<meta name="robots" content="noindex" />
{{ else }} {{ else }}
{{ if .url }}
<link rel="canonical" href="{{ .url }}" />
{{ end }}
<link rel="icon" href="/assets/logo/favicon.png" /> <link rel="icon" href="/assets/logo/favicon.png" />
{{ end }} {{ end }}
<link href="/assets/css/remixicon.css" rel="stylesheet" />
<script src="/assets/js/alpine.min.js" defer></script> <script src="/assets/js/alpine.min.js" defer></script>
<script src="/assets/js/htmx.min.js" defer></script> <script src="/assets/js/htmx.min.js" defer></script>
<script src="/assets/js/htmx-response-targets.js" defer></script> <script src="/assets/js/htmx-response-targets.js" defer></script>
<script src="/assets/js/client-side-templates.js" defer></script> <script src="/assets/js/mark.min.js" defer></script>
<script type="module" src="/assets/scripts.js"></script>
<link href="/assets/css/remixicon.css" rel="stylesheet" />
<link rel="stylesheet" type="text/css" href="/assets/css/fonts.css" /> <link rel="stylesheet" type="text/css" href="/assets/css/fonts.css" />
<link rel="stylesheet" type="text/css" href="/assets/style.css" /> <link rel="stylesheet" type="text/css" href="/assets/style.css" />
<script type="module"> <script type="module">
import { setup } from "/assets/scripts.js"; document.body.addEventListener("htmx:responseError", function (event) {
setup(); const config = event.detail.requestConfig;
if (config.boosted) {
document.body.innerHTML = event.detail.xhr.responseText;
const newUrl = event.detail.xhr.responseURL || config.url;
window.history.pushState(null, "", newUrl);
}
});
</script> </script>
</head> </head>
<body class="w-full" hx-ext="response-targets" hx-boost="true"> <body class="w-full h-full min-h-full" hx-ext="response-targets" hx-boost="true">
{{ block "body" . }} {{ block "body" . }}
<!-- Default app body... --> <!-- Default app body... -->
{{ end }} {{ end }}

View File

@@ -5,10 +5,16 @@
x-data="{ openeditionmenu: window.location.pathname.startsWith('/edition/')}"> x-data="{ openeditionmenu: window.location.pathname.startsWith('/edition/')}">
<div class="flex flex-row justify-between"> <div class="flex flex-row justify-between">
<div class="flex flex-row gap-x-3"> <div class="flex flex-row gap-x-3">
<div class="grow-0"><img class="h-14 w-14 border" src="/assets/favicon.png" /></div> <div class="grow-0">
<a href="/" class="no-underline"
><img class="h-14 w-14 border" src="/assets/favicon.png"
/></a>
</div>
<div class="flex flex-col"> <div class="flex flex-col">
<h1 class="font-bold text-2xl tracking-wide">{{ .site.title }}</h1> <h1 class="font-bold text-2xl tracking-wide">
<h2 class="italic">{{ .site.desc }}</h2> <a href="/" class="no-underline text-slate-800">{{ .site.title }}</a>
</h1>
<h2 class="italic text-slate-800">{{ .site.desc }}</h2>
</div> </div>
</div> </div>
@@ -40,7 +46,7 @@
{{ if and $model.page (HasPrefix $model.page.Path "/edition") -}} {{ if and $model.page (HasPrefix $model.page.Path "/edition") -}}
aria-current="true" aria-current="true"
{{- end }} {{- end }}
data-url="/edition/" data-url="/redaktion/"
class="text-slate-600 hover:text-slate-900 hover:cursor-pointer hover:bg-slate-100 class="text-slate-600 hover:text-slate-900 hover:cursor-pointer hover:bg-slate-100
!pr-2.5" !pr-2.5"
:class="openeditionmenu? 'bg-slate-100' : 'closed'" :class="openeditionmenu? 'bg-slate-100' : 'closed'"
@@ -57,36 +63,43 @@
class="submenu flex flex-row justify-end pt-3.5 gap-x-4 font-bold font-serif class="submenu flex flex-row justify-end pt-3.5 gap-x-4 font-bold font-serif
[&>a]:no-underline [&>*]:-mb-1.5 w-full pr-2.5 [&>*]:px-1.5"> [&>a]:no-underline [&>*]:-mb-1.5 w-full pr-2.5 [&>*]:px-1.5">
<a <a
href="/edition/einfuehrung/" href="/redaktion/einfuehrung/"
{{ if and $model.page (HasPrefix $model.page.Path "/edition/einfuehrung") -}} {{ if and $model.page (HasPrefix $model.page.Path "/redaktion/einfuehrung") -}}
aria-current="page" aria-current="page"
{{- end -}} {{- end -}}
>Einführung</a >Einführung</a
> >
<a <a
href="/edition/dokumentation/" href="/redaktion/dokumentation/"
{{ if and $model.page (HasPrefix $model.page.Path "/edition/dokumentation") -}} {{ if and $model.page (HasPrefix $model.page.Path "/redaktion/dokumentation") -}}
aria-current="page" aria-current="page"
{{- end -}} {{- end -}}
>Dokumentation</a >Dokumentation</a
> >
<a <a
href="/edition/literatur/" href="/redaktion/literatur/"
{{ if and $model.page (HasPrefix $model.page.Path "/edition/literatur") -}} {{ if and $model.page (HasPrefix $model.page.Path "/redaktion/literatur") -}}
aria-current="page" aria-current="page"
{{- end -}} {{- end -}}
>Literatur</a >Literatur</a
> >
<a <a
href="/edition/danksagungen/" href="/redaktion/lesekabinett/"
{{ if and $model.page (HasPrefix $model.page.Path "/edition/danksagungen") -}} {{ if and $model.page (HasPrefix $model.page.Path "/redaktion/lesekabinett") -}}
aria-current="page"
{{- end -}}
>Lesekabinett</a
>
<a
href="/redaktion/danksagungen/"
{{ if and $model.page (HasPrefix $model.page.Path "/redaktion/danksagungen") -}}
aria-current="page" aria-current="page"
{{- end -}} {{- end -}}
>Danksagungen</a >Danksagungen</a
> >
<a <a
href="/edition/kontakt/" href="/redaktion/kontakt/"
{{ if and $model.page (HasPrefix $model.page.Path "/edition/kontakt") -}} {{ if and $model.page (HasPrefix $model.page.Path "/redaktion/kontakt") -}}
aria-current="page" aria-current="page"
{{- end -}} {{- end -}}
>Kontakt</a >Kontakt</a

Binary file not shown.

View File

@@ -0,0 +1,39 @@
Joseph Franz von Ratschky: Vorbericht. in: Wiener Musenalmanach. 1779, S. 3-6.
ratschky_in_wiener_1779.pdf
Gottfried August Bürger: Nothgedrungene Nachrede. in: Göttinger Musenalmanach. 1782, S. 184-192.
buerger_in_goettinger_1782.pdf
Christian Cay Lorenz Hirschfeld: An die Leser. in: Gartenkalender. 1783, S. 272.
hirschfeld_in_gartenkalender_1783.pdf
Johann Heinrich Voß: Ankündigung. in: Hamburger Musenalmanach. 1784, S. 222ff.
voss_in_hamburger_1784.pdf
Gotthold Friedrich Stäudlin: Nachrede. in: Schwäbischer Musenalmanach. 1786 [o. S.].
staeudlin_in_schwaebischer_1786.pdf
Gottfried August Bürger: Fürbitte eines ans peinliche Kreuz der Verlegenheit genagelten Herausgebers eines Musenalmanachs. in: Göttinger Musenalmanach. 1789, S. 104.
buerger_in_goettinger_1789.pdf
Anonymus: Die deutschen Almanache. in: Bibliothek der redenden und bildenden Künste. Zweyten Bandes erstes Stück. Leipzig, in der Dyckischen Buchhandlung, 1806, S. 207-217.
anonymus.pdf
Stephan Schütze: Die Neujahrsversammlung. Ein dramatischer Prolog. in: Taschenbuch der Liebe und Freundschaft gewidmet. 1813, S. 1-20.
schuetze_in_taschenbuch_1813.pdf
N. B. E.: Die deutschen Taschenbücher für 1820. in: Hermes oder kritisches Jahrbuch der Literatur. Zweites Stück für das Jahr 1820. Amsterdam, in der Verlags-Expedition des Hermes, S. 191-235.
nbe_in_hermes_1820.pdf
Ferdinand Johannes Wit: Die Almanachomanie. in: Politisches Taschenbuch. 1831, S. 102-111.
wit_in_politaschenbuch_1831.pdf
August Wilhelm Schlegel: Recept. in: Deutscher Musenalmanach (Chamisso, Schwab, Gaudy). 1836, S. 18.
schlegel_in_deutscher_1836.pdf
Robert Eduard Prutz: Die Musenalmanache und Taschenbücher in Deutschland. in: Neue Schriften. Zur deutschen Literatur- und Kulturgeschichte. Erster Band, Halle, G. Schwetschke'scher Verlag, 1854, S. 105-165.
prutz_in_musenalmanache.pdf
Anonymus: Woher das Wort Almanach komme. in: Neues Wochenblatt zum Nuzzen und zur Unterhaltung für Kinder und junge Leute. Erstes Bändchen, erstes Stück, Leipzig, in der Sommerschen Buchhandlung 1794, S. 8f.
allatroia_anonymus_wochenblatt_1794.pdf
Allatroia

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -38,6 +38,14 @@
src: url(/assets/fonts/SourceSans3-BoldItalic.ttf) format("truetype"); src: url(/assets/fonts/SourceSans3-BoldItalic.ttf) format("truetype");
} }
@font-face {
font-family: "Spectral";
font-style: normal;
font-weight: 500;
font-display: swap;
src: url(/assets/fonts/Spectral-Regular.ttf) format("truetype");
}
@font-face { @font-face {
font-family: "Source Sans 3"; font-family: "Source Sans 3";
font-style: normal; font-style: normal;

Binary file not shown.

BIN
views/public/musen.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 249 KiB

View File

@@ -1 +1,53 @@
Hello from the root remplate! {{- $model := . -}}
<div id="intropageroot">
<image-reel class="hidden lg:block max-w-full my-8 mx-12 relative" id="imagecontainer">
<div class="overflow-hidden flex flex-row justify-between">
{{- range $i, $img := $model.bilder -}}
<div class="shrink-0 shadow-lg primages overflow-hidden w-[200px]">
<popup-image
data-image-url="{{- $img.BildPath -}}"
aria-role="button"
tabindex="0"
data-hide-dl-button="true">
<img
class="shadow h-full scale-[1.3]"
src="{{ $img.VorschauPath }}?thumb=500x0"
alt="musen" />
<div class="image-description hidden">
{{- Safe $img.Beschreibung -}}
</div>
</popup-image>
</div>
{{- end -}}
</div>
</image-reel>
<div class="w-full min-h-full mt-8">
<div class="text-center relative max-w-screen-xl mx-auto" data-="">
<img
src="/assets/musen.png"
class="max-w-[28rem] mx-auto lg:absolute left-2/3 top-2"
alt="Bild von aufsteigenden Musen" />
<a href="/reihen" class="block no-underline !font-[Spectral] small-caps text-slate-700">
<h1 class="text-2xl lg:text-4xl pt-8 lg:!pt-52 !font-[Spectral]">Willkommen auf der</h1>
<h1 class="text-4xl lg:text-8xl !font-[Spectral]">Musenalm</h1>
<h2 class="text-2xl lg:text-4xl !font-[Spectral]">Bibliographie deutscher Almanache</h2>
</a>
</div>
<div class="mt-8 lg:text-lg font-medium lg:w-9/12 xl:w-7/12 mx-auto px-4 lg:px-0">
<div class="lg:flex gap-x-12 starttext font-serif hyphens-auto indented">
<div class="lg:w-2/3">
{{- Safe $model.texte.Abs1 -}}
</div>
<div class="lg:mt-36 lg:w-1/3">
{{- Safe $model.texte.Abs2 -}}
</div>
</div>
<div class="text-center mt-8 startlinks font-serif text-xl" data-="">
<a href="/redaktion/einfuehrung" class="">Einleitung</a>
<div class="inline px-1">|</div>
<a href="/reihen" class="font-bold">Alle Bände<i class="ri-arrow-right-double-line"></i></a>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,8 @@
<div class="container-normal relative">
<div class="text indented">
{{ if .record.Title }}<h1 class="mb-12">{{ .record.Title }}</h1>{{ end }}
<div class="text">
{{ Safe .record.Text }}
</div>
</div>
</div>

View File

@@ -2,10 +2,10 @@
<div class="container-normal relative"> <div class="container-normal relative">
<div class="text indented"> <div class="text ">
{{ if .record.Title }}<h1 class="mb-12">{{ .record.Title }}</h1>{{ end }} {{ if .record.Title }}<h1 class="mb-12">{{ .record.Title }}</h1>{{ end }}
<div class="flex flex-row gap-x-6 justify-between"> <div class="flex flex-row gap-x-6 justify-between ">
<div class="jumptext grow shrink-0"> <div class="grow shrink-0 text indented">
{{ Safe .record.Text }} {{ Safe .record.Text }}
</div> </div>
<div> <div>

View File

@@ -0,0 +1 @@
<title>{{ .site.title }} &ndash; {{ .record.Title }}</title>

View File

@@ -10,16 +10,16 @@
<div class="px-8"> <div class="px-8">
{{ Safe $model.record.Text }} {{ Safe $model.record.Text }}
<div class="pt-3"> <div class="pt-3">
<a href="/edition/einfuehrung">Einführung</i></a> <a href="/redaktion/einfuehrung">Einführung</i></a>
<i class="ri-seedling-line px-1.5"></i> <i class="ri-seedling-line px-1.5"></i>
<a href="/edition/dokumentation">Dokumentation </a> <a href="/redaktion/dokumentation">Dokumentation </a>
</div> </div>
<div class="mt-4 py-2 px-3 rounded bg-orange-100 border border-orange-200 <div class="mt-4 py-2 px-3 rounded bg-orange-100 border border-orange-200
text-orange-950 font-sans font-bold"> text-orange-950 font-sans font-bold">
Bitte beachten Sie, dass es sich hier noch um eine öffentliche Testversion Bitte beachten Sie, dass es sich hier noch um eine öffentliche Testversion
handelt. Über Rückmeldungen und Anregungen freuen wir uns [&rarr; <a handelt. Über Rückmeldungen und Anregungen freuen wir uns [&rarr; <a
href="/edition//kontakt">Kontakt</a>] href="/redaktion//kontakt">Kontakt</a>]
</div> </div>
</div> </div>
</div> </div>

View File

@@ -185,7 +185,6 @@
{{- else -}} {{- else -}}
<div class="mt-4">Kein Beitrag gefunden.</div> <div class="mt-4">Kein Beitrag gefunden.</div>
{{- end -}} {{- end -}}
</div>
<script type="module"> <script type="module">
let elements = document.querySelectorAll('.search-text'); let elements = document.querySelectorAll('.search-text');
@@ -198,4 +197,5 @@
}, 200); }, 200);
</script> </script>
</div> </div>
</div>
{{- end -}} {{- end -}}

View File

@@ -5,7 +5,9 @@
-}} -}}
{{- if $isFiltered -}} {{- if $isFiltered -}}
<div class="flex flex-row gap-x-3" id="searchpills"> <div
class="flex flex-row gap-x-3 bg-orange-100 items-center py-1 justify-between"
id="searchpills">
{{- if $model.filters.Agent -}} {{- if $model.filters.Agent -}}
<filter-pill <filter-pill
data-queryparam="agentfilter" data-queryparam="agentfilter"
@@ -28,6 +30,7 @@
</filter-pill> </filter-pill>
{{- end -}} {{- end -}}
{{- if $model.filters.OnlyScans -}} {{- if $model.filters.OnlyScans -}}
<div class="grow"></div>
<filter-pill <filter-pill
data-queryparam="onlyscans" data-queryparam="onlyscans"
data-value="{{ $model.filters.OnlyScans }}" data-value="{{ $model.filters.OnlyScans }}"

View File

@@ -18,6 +18,7 @@ const INT_LINK_ELEMENT = "int-link";
const POPUP_IMAGE_ELEMENT = "popup-image"; const POPUP_IMAGE_ELEMENT = "popup-image";
const TABLIST_ELEMENT = "tab-list"; const TABLIST_ELEMENT = "tab-list";
const FILTER_PILL_ELEMENT = "filter-pill"; const FILTER_PILL_ELEMENT = "filter-pill";
const IMAGE_REEL_ELEMENT = "image-reel";
class XSLTParseProcess { class XSLTParseProcess {
#processors; #processors;
@@ -180,10 +181,8 @@ class FilterPill extends HTMLElement {
render() { render() {
this.innerHTML = ` this.innerHTML = `
<a href="${this.getURL()}" class="!no-underline block text-base" hx-target="#searchresults" hx-select="#searchresults" hx-swap="outerHTML show:window:top"> <a href="${this.getURL()}" class="!no-underline block text-base" hx-target="#searchresults" hx-select="#searchresults" hx-swap="outerHTML show:window:top">
<div class="flex flex-row filter-pill rounded-lg bg-orange-100 hover:saturate-50 px-2.5 mt-2"> <div class="flex flex-row filter-pill rounded-lg bg-orange-100 hover:saturate-50 px-2.5">
<div href="${this.getURL()}" class="filter-pill-close no-underline font-bold mr-1 text-orange-900 hover:text-orange-800"> ${this.renderIcon()}
<i class="ri-close-circle-line"></i>
</div>
<div class="flex flex-row filter-pill-label-value !items-baseline text-slate-700"> <div class="flex flex-row filter-pill-label-value !items-baseline text-slate-700">
<div class="filter-pill-label font-bold mr-1.5 align-baseline">${this.text}</div> <div class="filter-pill-label font-bold mr-1.5 align-baseline">${this.text}</div>
${this.renderValue()} ${this.renderValue()}
@@ -193,6 +192,23 @@ class FilterPill extends HTMLElement {
`; `;
} }
renderIcon() {
const isBool = this.value === "true" || this.value === "false";
if (!isBool) {
return `<div
href="${this.getURL()}"
class="filter-pill-close no-underline font-bold mr-1 text-orange-900 hover:text-orange-800">
<i class="ri-arrow-left-s-line"></i>
</div>
`;
}
return `
<div href="${this.getURL()}" class="filter-pill-close no-underline font-bold mr-1 text-orange-900 hover:text-orange-800">
<i class="ri-close-circle-line"></i>
</div>
`;
}
renderValue() { renderValue() {
const isBool = this.value === "true" || this.value === "false"; const isBool = this.value === "true" || this.value === "false";
if (isBool) return ``; if (isBool) return ``;
@@ -713,10 +729,14 @@ class PopupImage extends HTMLElement {
this._preview = null; this._preview = null;
this._description = null; this._description = null;
this._imageURL = ""; this._imageURL = "";
this._hideDLButton = false;
} }
connectedCallback() { connectedCallback() {
this.classList.add("cursor-pointer");
this.classList.add("select-none");
this._imageURL = this.getAttribute("data-image-url") || ""; this._imageURL = this.getAttribute("data-image-url") || "";
this._hideDLButton = this.getAttribute("data-hide-dl-button") || false;
this._preview = this.querySelector("img"); this._preview = this.querySelector("img");
this._description = this.querySelector(".image-description"); this._description = this.querySelector(".image-description");
@@ -735,7 +755,6 @@ class PopupImage extends HTMLElement {
} }
showOverlay() { showOverlay() {
const descriptionHtml = this._description ? this._description.innerHTML : "";
this.overlay = document.createElement("div"); this.overlay = document.createElement("div");
this.overlay.classList.add( this.overlay.classList.add(
"fixed", "fixed",
@@ -749,25 +768,20 @@ class PopupImage extends HTMLElement {
); );
this.overlay.innerHTML = ` this.overlay.innerHTML = `
<div class="relative w-max max-w-dvw max-h-dvh shadow-lg flex flex-col items-center gap-4"> <div class="relative w-max max-w-dvw max-h-dvh shadow-lg flex flex-col items-center justify-center gap-4">
<div class="absolute -right-16 top-0 text-white text-4xl flex flex-col"> <div>
<div class="absolute -right-16 text-white text-4xl flex flex-col">
<button class="hover:text-gray-300 cursor-pointer focus:outline-none" aria-label="Close popup"> <button class="hover:text-gray-300 cursor-pointer focus:outline-none" aria-label="Close popup">
<i class="ri-close-fill text-4xl"></i> <i class="ri-close-fill text-4xl"></i>
</button> </button>
<tool-tip position="right"> ${this.downloadButton()}
<a href="${this._imageURL}" target="_blank" class="text-white no-underline hover:text-gray-300"><i class="ri-file-download-line"></i></a>
<div class="data-tip">Bild herunterladen</div>
</tool-tip>
</div> </div>
<img <img
src="${this._imageURL}" src="${this._imageURL}"
alt="Popup Image" alt="Popup Image"
class="full max-h-[90vh] max-w-[80vw] object-contain" class="full max-h-[80vh] max-w-[80vw] object-contain block relative ${this.descriptionImgClass()}"
/> />
${this.description()}
<div class="text-center text-gray-700 description-content">
${descriptionHtml}
</div> </div>
</div> </div>
`; `;
@@ -788,6 +802,40 @@ class PopupImage extends HTMLElement {
document.body.appendChild(this.overlay); document.body.appendChild(this.overlay);
} }
descriptionImgClass() {
if (!this.description) {
return "0";
}
return "";
}
description() {
if (!this._description) {
return "";
}
return `
<div class="font-serif text-left description-content mt-3 text-slate-900 ">
<div class="max-w-[80ch] hyphens-auto px-6 py-2 bg-stone-50 shadow-lg">
${this._description.innerHTML}
</div>
</div>
`;
}
downloadButton() {
if (this._hideDLButton) {
return "";
}
return `
<tool-tip position="right">
<a href="${this._imageURL}" target="_blank" class="text-white no-underline hover:text-gray-300"><i class="ri-file-download-line"></i></a>
<div class="data-tip">Bild herunterladen</div>
</tool-tip>
`;
}
hideOverlay() { hideOverlay() {
this.overlay.parentNode.removeChild(this.overlay); this.overlay.parentNode.removeChild(this.overlay);
this.overlay = null; this.overlay = null;
@@ -1167,6 +1215,43 @@ class IntLink extends HTMLElement {
} }
} }
class ImageReel extends HTMLElement {
#minWidth = 176;
constructor() {
super();
this._images = [];
}
connectedCallback() {
this._images = Array.from(this.querySelectorAll(".primages"));
this.calculateShownImages();
const rObs = new ResizeObserver((__, _) => {
this.calculateShownImages();
});
this._resizeObserver = rObs;
rObs.observe(this);
}
disconnectedCallback() {
this._resizeObserver.unobserve(this);
}
calculateShownImages() {
const c = this.getBoundingClientRect();
console.log(c);
const fits = Math.floor(c.width / (this.#minWidth + 10));
for (let i = 0; i < this._images.length; i++) {
if (i < fits - 1) {
this._images[i].classList.remove("hidden");
} else {
this._images[i].classList.add("hidden");
}
}
}
}
customElements.define(INT_LINK_ELEMENT, IntLink); customElements.define(INT_LINK_ELEMENT, IntLink);
customElements.define(ABBREV_TOOLTIPS_ELEMENT, AbbreviationTooltips); customElements.define(ABBREV_TOOLTIPS_ELEMENT, AbbreviationTooltips);
customElements.define(FILTER_LIST_ELEMENT, FilterList); customElements.define(FILTER_LIST_ELEMENT, FilterList);
@@ -1175,5 +1260,6 @@ customElements.define(TOOLTIP_ELEMENT, ToolTip);
customElements.define(POPUP_IMAGE_ELEMENT, PopupImage); customElements.define(POPUP_IMAGE_ELEMENT, PopupImage);
customElements.define(TABLIST_ELEMENT, Tablist); customElements.define(TABLIST_ELEMENT, Tablist);
customElements.define(FILTER_PILL_ELEMENT, FilterPill); customElements.define(FILTER_PILL_ELEMENT, FilterPill);
customElements.define(IMAGE_REEL_ELEMENT, ImageReel);
export { XSLTParseProcess, FilterList, ScrollButton, AbbreviationTooltips }; export { XSLTParseProcess, FilterList, ScrollButton, AbbreviationTooltips };

View File

@@ -242,6 +242,11 @@
@apply -indent-3.5 ml-3.5; @apply -indent-3.5 ml-3.5;
} }
#indexpage {
background-image: url("/assets/bg.jpg");
@apply h-full w-full;
}
#searchnav > a:nth-of-type(1) { #searchnav > a:nth-of-type(1) {
@apply ml-6; @apply ml-6;
} }