+Abkürzungen

This commit is contained in:
Simon Martens
2026-01-12 18:57:34 +01:00
parent 696f7fe087
commit 7d7637fe13
21 changed files with 465 additions and 204 deletions

View File

@@ -4,6 +4,7 @@ import (
"database/sql" "database/sql"
"fmt" "fmt"
"github.com/Theodor-Springmann-Stiftung/musenalm/dbmodels"
"github.com/Theodor-Springmann-Stiftung/musenalm/middleware" "github.com/Theodor-Springmann-Stiftung/musenalm/middleware"
"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"
@@ -147,6 +148,14 @@ func (app *App) createEngine() (*templating.Engine, error) {
"desc": "Bibliographie deutscher Almanache des 18. und 19. Jahrhunderts", "desc": "Bibliographie deutscher Almanache des 18. und 19. Jahrhunderts",
}}) }})
engine.AddFunc("data", func(key string) any {
res, err := dbmodels.Data_Key(app.PB.App, key)
if err != nil {
return "{}"
}
return res.Value()
})
return engine, nil return engine, nil
} }

210
controllers/abkuerzungen.go Normal file
View File

@@ -0,0 +1,210 @@
package controllers
import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"slices"
"strings"
"github.com/Theodor-Springmann-Stiftung/musenalm/app"
"github.com/Theodor-Springmann-Stiftung/musenalm/dbmodels"
"github.com/Theodor-Springmann-Stiftung/musenalm/middleware"
"github.com/Theodor-Springmann-Stiftung/musenalm/pagemodels"
"github.com/Theodor-Springmann-Stiftung/musenalm/templating"
"github.com/pocketbase/pocketbase/core"
"github.com/pocketbase/pocketbase/tools/router"
"golang.org/x/text/collate"
"golang.org/x/text/language"
)
const (
URL_ABKUERZUNGEN = "/abkuerzungen/"
TEMPLATE_ABKUERZUNGEN = "/abkuerzungen/"
)
func init() {
ap := &AbkuerzungenPage{
StaticPage: pagemodels.StaticPage{
Name: pagemodels.P_ABKUERZUNGEN_NAME,
URL: URL_ABKUERZUNGEN,
Template: TEMPLATE_ABKUERZUNGEN,
Layout: templating.DEFAULT_LAYOUT_NAME,
},
}
app.Register(ap)
}
type AbkuerzungenPage struct {
pagemodels.StaticPage
}
type AbkEntry struct {
Key string
Value string
}
type AbkuerzungenResult struct {
Entries []AbkEntry
}
func (p *AbkuerzungenPage) Setup(router *router.Router[*core.RequestEvent], app core.App, engine *templating.Engine) error {
router.GET(URL_ABKUERZUNGEN, p.GET(engine, app))
rg := router.Group(URL_ABKUERZUNGEN)
rg.BindFunc(middleware.IsAdminOrEditor())
rg.POST("", p.POST(engine, app))
return nil
}
func (p *AbkuerzungenPage) GET(engine *templating.Engine, app core.App) HandleFunc {
return func(e *core.RequestEvent) error {
// Read abbreviations from data table
dataRecord, err := dbmodels.Data_Key(app, "abkuerzungen")
if err != nil {
return engine.Response500(e, err, nil)
}
// Parse JSON value into map
abkMap := make(map[string]string)
rawValue := dataRecord.Value()
// Convert via JSON marshal/unmarshal to handle types.JSONRaw
jsonBytes, err := json.Marshal(rawValue)
if err != nil {
app.Logger().Error("Failed to marshal abkürzungen", "error", err)
return engine.Response500(e, err, nil)
}
if err := json.Unmarshal(jsonBytes, &abkMap); err != nil {
app.Logger().Error("Failed to unmarshal abkürzungen", "error", err)
return engine.Response500(e, err, nil)
}
// Convert to sorted array
entries := []AbkEntry{}
for k, v := range abkMap {
entries = append(entries, AbkEntry{Key: k, Value: v})
}
// Sort by key (German collation)
collator := collate.New(language.German)
slices.SortFunc(entries, func(a, b AbkEntry) int {
return collator.CompareString(a.Key, b.Key)
})
data := map[string]any{
"result": &AbkuerzungenResult{Entries: entries},
}
req := templating.NewRequest(e)
if req.Session() != nil {
data["csrf_token"] = req.Session().Token
} else {
data["csrf_token"] = ""
}
if msg := e.Request.URL.Query().Get("success"); msg != "" {
data["success"] = msg
}
if msg := e.Request.URL.Query().Get("error"); msg != "" {
data["error"] = msg
}
return engine.Response200(e, p.Template, data, p.Layout)
}
}
type abkFormEntry struct {
Key string `form:"key"`
Value string `form:"value"`
Delete string `form:"_delete"`
}
func (p *AbkuerzungenPage) POST(engine *templating.Engine, app core.App) HandleFunc {
return func(e *core.RequestEvent) error {
req := templating.NewRequest(e)
// Parse form manually to handle array syntax
if err := e.Request.ParseForm(); err != nil {
return p.redirectError(e, "Formulardaten ungültig.")
}
csrfToken := e.Request.FormValue("csrf_token")
// Validate CSRF
if err := req.CheckCSRF(csrfToken); err != nil {
return p.redirectError(e, err.Error())
}
// Build new map from form data
newMap := make(map[string]string)
// Parse array fields manually
form := e.Request.Form
i := 0
for {
keyField := fmt.Sprintf("abkuerzungen[%d][key]", i)
valueField := fmt.Sprintf("abkuerzungen[%d][value]", i)
deleteField := fmt.Sprintf("abkuerzungen[%d][_delete]", i)
key := form.Get(keyField)
value := form.Get(valueField)
delete := form.Get(deleteField)
// No more entries
if key == "" && value == "" {
break
}
app.Logger().Info("Form entry", "i", i, "key", key, "value", value, "delete", delete)
i++
// Skip deleted or empty entries
if delete == "true" || strings.TrimSpace(key) == "" {
continue
}
key = strings.TrimSpace(key)
value = strings.TrimSpace(value)
// Validate required value
if value == "" {
return p.redirectError(e, fmt.Sprintf("Abkürzung '%s' benötigt eine Beschreibung.", key))
}
// Check for duplicates
if _, exists := newMap[key]; exists {
return p.redirectError(e, fmt.Sprintf("Doppelter Schlüssel: '%s'", key))
}
newMap[key] = value
}
app.Logger().Info("Saving abkürzungen", "count", len(newMap))
// Update data record
dataRecord, err := dbmodels.Data_Key(app, "abkuerzungen")
if err != nil {
app.Logger().Error("Failed to load abkuerzungen record", "error", err)
return p.redirectError(e, "Laden der Daten fehlgeschlagen.")
}
dataRecord.Set(dbmodels.VALUE_FIELD, newMap)
if err := app.Save(dataRecord); err != nil {
app.Logger().Error("Failed to save abkuerzungen", "error", err)
return p.redirectError(e, "Speichern fehlgeschlagen.")
}
// Redirect with success message
redirect := fmt.Sprintf("%s?success=%s", URL_ABKUERZUNGEN, url.QueryEscape("Änderungen gespeichert."))
return e.Redirect(http.StatusSeeOther, redirect)
}
}
func (p *AbkuerzungenPage) redirectError(e *core.RequestEvent, message string) error {
redirect := fmt.Sprintf("%s?error=%s", URL_ABKUERZUNGEN, url.QueryEscape(message))
return e.Redirect(http.StatusSeeOther, redirect)
}

View File

@@ -50,11 +50,6 @@ func (p *AlmanachPage) GET(engine *templating.Engine, app core.App) HandleFunc {
data["result"] = result data["result"] = result
data["filters"] = filters data["filters"] = filters
abbrs, err := pagemodels.GetAbks(app)
if err == nil {
data["abbrs"] = abbrs
}
return engine.Response200(e, p.Template, data) return engine.Response200(e, p.Template, data)
} }
} }

View File

@@ -66,11 +66,6 @@ func (p *AlmanachEditPage) GET(engine *templating.Engine, app core.App) HandleFu
data["agent_relations"] = dbmodels.AGENT_RELATIONS data["agent_relations"] = dbmodels.AGENT_RELATIONS
data["series_relations"] = dbmodels.SERIES_RELATIONS data["series_relations"] = dbmodels.SERIES_RELATIONS
abbrs, err := pagemodels.GetAbks(app)
if err == nil {
data["abbrs"] = abbrs
}
if msg := e.Request.URL.Query().Get("saved_message"); msg != "" { if msg := e.Request.URL.Query().Get("saved_message"); msg != "" {
data["success"] = msg data["success"] = msg
} }

View File

@@ -83,11 +83,6 @@ func (p *AlmanachNewPage) GET(engine *templating.Engine, app core.App) HandleFun
data["series_relations"] = dbmodels.SERIES_RELATIONS data["series_relations"] = dbmodels.SERIES_RELATIONS
data["is_new"] = true data["is_new"] = true
abbrs, err := pagemodels.GetAbks(app)
if err == nil {
data["abbrs"] = abbrs
}
return engine.Response200(e, p.Template, data, p.Layout) return engine.Response200(e, p.Template, data, p.Layout)
} }
} }

View File

@@ -34,18 +34,13 @@ type BeitragPage struct {
func (p *BeitragPage) Setup(router *router.Router[*core.RequestEvent], app core.App, engine *templating.Engine) error { func (p *BeitragPage) Setup(router *router.Router[*core.RequestEvent], app core.App, engine *templating.Engine) error {
router.GET(p.URL, func(e *core.RequestEvent) error { router.GET(p.URL, func(e *core.RequestEvent) error {
id := e.Request.PathValue("id") id := e.Request.PathValue("id")
data := make(map[string]interface{}) data := make(map[string]any)
result, err := NewBeitragResult(app, id) result, err := NewBeitragResult(app, id)
if err != nil { if err != nil {
engine.Response404(e, err, nil) engine.Response404(e, err, nil)
} }
data["result"] = result data["result"] = result
abbrs, err := pagemodels.GetAbks(app)
if err == nil {
data["abbrs"] = abbrs
}
return engine.Response200(e, p.Template, data) return engine.Response200(e, p.Template, data)
}) })

View File

@@ -14,14 +14,10 @@ func (d *Data) SetKey(key string) {
d.Set(KEY_FIELD, key) d.Set(KEY_FIELD, key)
} }
func (d *Data) Value() map[string]interface{} { func (d *Data) Value() any {
val := d.Get(VALUE_FIELD) return d.GetRaw(VALUE_FIELD)
if val == nil {
return nil
}
return val.(map[string]interface{})
} }
func (d *Data) SetValue(value map[string]interface{}) { func (d *Data) SetValue(value string) {
d.Set(VALUE_FIELD, value) d.Set(VALUE_FIELD, value)
} }

View File

@@ -501,6 +501,7 @@ const (
REFERENCES_FIELD = "refs" REFERENCES_FIELD = "refs"
URI_FIELD = "uri" URI_FIELD = "uri"
URL_FIELD ="url"
MUSENALM_BAENDE_STATUS_FIELD = "musenalm_status" MUSENALM_BAENDE_STATUS_FIELD = "musenalm_status"
MUSENALM_INHALTE_TYPE_FIELD = "musenalm_type" MUSENALM_INHALTE_TYPE_FIELD = "musenalm_type"

View File

@@ -144,6 +144,11 @@ func Sessions_ID(app core.App, id string) (*Session, error) {
return &ret, err return &ret, err
} }
func Data_Key(app core.App, key string) (*Data, error) {
ret, err := TableByField[Data](app, DATA_TABLE, KEY_FIELD, key)
return &ret, err
}
func AccessTokens_Token(app core.App, token string) (*AccessToken, error) { func AccessTokens_Token(app core.App, token string) (*AccessToken, error) {
t := HashStringSHA256(token) t := HashStringSHA256(token)
return TableByField[*AccessToken]( return TableByField[*AccessToken](

View File

@@ -1,6 +1,7 @@
package functions package functions
import ( import (
"encoding/json"
"fmt" "fmt"
"html/template" "html/template"
"regexp" "regexp"
@@ -19,6 +20,15 @@ func Safe(s string) template.HTML {
return template.HTML(s) return template.HTML(s)
} }
func SafeJS(s any) template.JS {
b, err := json.Marshal(s)
if err != nil {
return template.JS("{}")
}
return template.JS(b)
}
func ReplaceSlashParen(s string) string { func ReplaceSlashParen(s string) string {
return strings.ReplaceAll(s, "/)", "<p>") return strings.ReplaceAll(s, "/)", "<p>")
} }

View File

@@ -1,9 +1,6 @@
package migrations package migrations
import ( import (
"errors"
"github.com/Theodor-Springmann-Stiftung/musenalm/dbmodels"
"github.com/Theodor-Springmann-Stiftung/musenalm/pagemodels" "github.com/Theodor-Springmann-Stiftung/musenalm/pagemodels"
"github.com/pocketbase/pocketbase/core" "github.com/pocketbase/pocketbase/core"
m "github.com/pocketbase/pocketbase/migrations" m "github.com/pocketbase/pocketbase/migrations"
@@ -21,12 +18,6 @@ func init() {
return err return err
} }
abk := abkCollection()
if err := app.Save(abk); err != nil {
app.Logger().Error("Failed to save collection:", "error", err, "collection", abk)
return err
}
return nil return nil
}, func(app core.App) error { }, func(app core.App) error {
collection, err := app.FindCollectionByNameOrId( collection, err := app.FindCollectionByNameOrId(
@@ -37,14 +28,7 @@ func init() {
} }
} }
collection_abk, err2 := app.FindCollectionByNameOrId( return err
pagemodels.GeneratePageTableName(pagemodels.P_DOK_NAME, pagemodels.T_ABK_NAME))
if err == nil && collection_abk != nil {
if err := app.Delete(collection_abk); err != nil {
app.Logger().Error("Failed to delete collection:", "error", err, "collection", collection_abk)
}
}
return errors.Join(err, err2)
}) })
} }
@@ -53,13 +37,3 @@ func dokCollection() *core.Collection {
c.Fields = append(c.Fields, dok_fields...) c.Fields = append(c.Fields, dok_fields...)
return c return c
} }
func abkCollection() *core.Collection {
c := core.NewBaseCollection(pagemodels.GeneratePageTableName(pagemodels.P_DOK_NAME, pagemodels.T_ABK_NAME))
c.Fields = core.NewFieldsList(
pagemodels.RequiredTextField(pagemodels.F_ABK),
pagemodels.RequiredTextField(pagemodels.F_BEDEUTUNG),
)
dbmodels.SetBasicPublicRules(c)
return c
}

View File

@@ -2,11 +2,11 @@ package migrations
import ( import (
"bufio" "bufio"
"errors"
"fmt" "fmt"
"os" "os"
"strings" "strings"
"github.com/Theodor-Springmann-Stiftung/musenalm/dbmodels"
"github.com/Theodor-Springmann-Stiftung/musenalm/pagemodels" "github.com/Theodor-Springmann-Stiftung/musenalm/pagemodels"
"github.com/pocketbase/pocketbase/core" "github.com/pocketbase/pocketbase/core"
m "github.com/pocketbase/pocketbase/migrations" m "github.com/pocketbase/pocketbase/migrations"
@@ -505,19 +505,7 @@ func init() {
return err return err
} }
abk, err := seed_abkuerzungen(app) return seed_abkuerzungen(app)
if err != nil {
app.Logger().Error("Failed to seed abkuerzungen", "error", err)
return err
}
for _, a := range abk {
if err := app.Save(a); err != nil {
app.Logger().Error("Failed to save abk", "error", err, "abk", a)
}
}
return nil
}, func(app core.App) error { }, func(app core.App) error {
coll, err := app.FindCollectionByNameOrId( coll, err := app.FindCollectionByNameOrId(
pagemodels.GeneratePageTableName(pagemodels.P_DOK_NAME)) pagemodels.GeneratePageTableName(pagemodels.P_DOK_NAME))
@@ -526,30 +514,19 @@ func init() {
app.DB().NewQuery("DELETE FROM " + coll.TableName()).Execute() app.DB().NewQuery("DELETE FROM " + coll.TableName()).Execute()
} }
coll_abk, err2 := app.FindCollectionByNameOrId( return err
pagemodels.GeneratePageTableName(pagemodels.P_DOK_NAME, pagemodels.T_ABK_NAME))
if err == nil && coll_abk != nil {
app.DB().NewQuery("DELETE FROM " + coll_abk.TableName()).Execute()
}
return errors.Join(err, err2)
}) })
} }
func seed_abkuerzungen(app core.App) ([]*pagemodels.Abk, error) { func seed_abkuerzungen(app core.App) error {
collection, err := app.FindCollectionByNameOrId(pagemodels.GeneratePageTableName(pagemodels.P_DOK_NAME, pagemodels.T_ABK_NAME))
if err != nil {
return nil, err
}
if _, err := os.Stat(ABK_PATH); err != nil { if _, err := os.Stat(ABK_PATH); err != nil {
return nil, err return err
} }
file, err := os.Open(ABK_PATH) file, err := os.Open(ABK_PATH)
if err != nil { if err != nil {
return nil, err return err
} }
defer file.Close() defer file.Close()
@@ -568,13 +545,15 @@ func seed_abkuerzungen(app core.App) ([]*pagemodels.Abk, error) {
abk[split[0]] = strings.TrimSpace(besch) abk[split[0]] = strings.TrimSpace(besch)
} }
ret := make([]*pagemodels.Abk, 0, len(abk)) dataColl, err := app.FindCollectionByNameOrId(dbmodels.DATA_TABLE)
for a, b := range abk { if err != nil {
r := pagemodels.NewAbk(core.NewRecord(collection)) return err
r.SetAbk(a)
r.SetBedeutung(b)
ret = append(ret, r)
} }
return ret, nil // Create new record in data table
record := core.NewRecord(dataColl)
record.Set(dbmodels.KEY_FIELD, "abkuerzungen")
record.Set(dbmodels.VALUE_FIELD, abk)
return app.Save(record)
} }

View File

@@ -1,56 +0,0 @@
package migrations
import (
"github.com/Theodor-Springmann-Stiftung/musenalm/dbmodels"
"github.com/pocketbase/pocketbase/core"
m "github.com/pocketbase/pocketbase/migrations"
)
func init() {
m.Register(func(app core.App) error {
// Find source collection
abkColl, err := app.FindCollectionByNameOrId("page_benutzerhinweise_abkuerzungen")
if err != nil {
return err
}
// Query all abbreviations
var records []*core.Record
err = app.RecordQuery(abkColl.Name).All(&records)
if err != nil {
return err
}
// Convert to JSON map
abkMap := make(map[string]string)
for _, r := range records {
abkMap[r.GetString("Abkuerzung")] = r.GetString("Bedeutung")
}
// Find data collection
dataColl, err := app.FindCollectionByNameOrId(dbmodels.DATA_TABLE)
if err != nil {
return err
}
// Create new record in data table
record := core.NewRecord(dataColl)
record.Set(dbmodels.KEY_FIELD, "abkuerzungen")
record.Set(dbmodels.VALUE_FIELD, abkMap)
return app.Save(record)
}, func(app core.App) error {
// Rollback: delete from data table
dataColl, err := app.FindCollectionByNameOrId(dbmodels.DATA_TABLE)
if err != nil {
return err
}
record, err := app.FindFirstRecordByFilter(dataColl.Name, dbmodels.KEY_FIELD+" = 'abkuerzungen'")
if err != nil {
return nil // Already deleted
}
return app.Delete(record)
})
}

View File

@@ -1,49 +0,0 @@
package pagemodels
import "github.com/pocketbase/pocketbase/core"
type Abk struct {
core.BaseRecordProxy
}
func (a *Abk) TableName() string {
return GeneratePageTableName(P_DOK_NAME, T_ABK_NAME)
}
func NewAbk(record *core.Record) *Abk {
i := &Abk{}
i.SetProxyRecord(record)
return i
}
func (a *Abk) Abk() string {
return a.GetString(F_ABK)
}
func (a *Abk) SetAbk(abk string) {
a.Set(F_ABK, abk)
}
func (a *Abk) Bedeutung() string {
return a.GetString(F_BEDEUTUNG)
}
func (a *Abk) SetBedeutung(bedeutung string) {
a.Set(F_BEDEUTUNG, bedeutung)
}
func GetAbks(app core.App) (map[string]string, error) {
ret := make(map[string]string)
abks := []*Abk{}
err := app.RecordQuery(GeneratePageTableName(P_DOK_NAME, T_ABK_NAME)).All(&abks)
if err != nil {
return ret, err
}
for _, abk := range abks {
ret[abk.Abk()] = abk.Bedeutung()
}
return ret, nil
}

View File

@@ -14,16 +14,13 @@ const (
P_REIHEN_NAME = "reihen" P_REIHEN_NAME = "reihen"
P_ORTE_NAME = "orte" P_ORTE_NAME = "orte"
P_ABKUERZUNGEN_NAME = "abkuerzungen"
P_DANK_NAME = "danksagungen" P_DANK_NAME = "danksagungen"
P_KONTAKT_NAME = "kontakt" P_KONTAKT_NAME = "kontakt"
P_LIT_NAME = "literatur" P_LIT_NAME = "literatur"
P_EINFUEHRUNG_NAME = "einleitung" P_EINFUEHRUNG_NAME = "einleitung"
P_DOK_NAME = "benutzerhinweise" P_DOK_NAME = "benutzerhinweise"
T_ABK_NAME = "abkuerzungen"
F_ABK = "Abkuerzung"
F_BEDEUTUNG = "Bedeutung"
F_TITLE = "Titel" F_TITLE = "Titel"
F_DESCRIPTION = "Beschreibung" F_DESCRIPTION = "Beschreibung"

View File

@@ -115,6 +115,7 @@ func (e *Engine) funcs() error {
// Passing HTML // Passing HTML
e.AddFunc("Safe", functions.Safe) e.AddFunc("Safe", functions.Safe)
e.AddFunc("SafeJS", functions.SafeJS)
// Creating an array or dict (to pass to a template) // Creating an array or dict (to pass to a template)
e.AddFunc("Arr", functions.Arr) e.AddFunc("Arr", functions.Arr)
e.AddFunc("Dict", functions.Dict) e.AddFunc("Dict", functions.Dict)

View File

@@ -8795,6 +8795,15 @@ class Qc extends HTMLElement {
<i class="ri-external-link-line text-base"></i> <i class="ri-external-link-line text-base"></i>
</a> </a>
</div> </div>
<div class="grid grid-cols-[1fr_auto] group">
<a href="/abkuerzungen/" class="flex items-center px-4 py-2 group-hover:bg-gray-100 transition-colors no-underline text-sm">
<i class="ri-text text-base text-gray-700 mr-2.5"></i>
<span class="text-gray-900">Abkürzungen</span>
</a>
<a href="/abkuerzungen/" target="_blank" class="flex items-center justify-center px-3 py-2 group-hover:bg-gray-100 text-gray-700 hover:text-slate-900 transition-colors no-underline text-sm" title="In neuem Tab öffnen">
<i class="ri-external-link-line text-base"></i>
</a>
</div>
<div class="border-t border-gray-200 my-1"></div> <div class="border-t border-gray-200 my-1"></div>
` : "", w = r ? ` ` : "", w = r ? `
<div class="px-3 py-1.5 text-xs font-semibold text-gray-500 uppercase tracking-wider"> <div class="px-3 py-1.5 text-xs font-semibold text-gray-500 uppercase tracking-wider">

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,186 @@
{{ $model := . }}
<edit-page>
<div class="flex container-normal bg-slate-100 mx-auto px-8">
<div class="flex flex-row w-full justify-between">
<div class="flex flex-col justify-end-safe flex-2/5">
<div class="mb-1">
<i class="ri-text"></i> Verwaltung
</div>
<h1 class="text-2xl w-full font-bold text-slate-900 mb-1">
Abkürzungen
</h1>
<div class="flex flex-row gap-x-3">
<div>
<a href="/abkuerzungen/" class="text-gray-700 hover:text-slate-950 block no-underline">
<i class="ri-loop-left-line"></i> Reset
</a>
</div>
</div>
</div>
</div>
</div>
<div class="container-normal mx-auto mt-4 !px-0">
{{ template "_usermessage" $model }}
{{- if (IsAdminOrEditor $model.request.user) -}}
{{/* Editable form for admin/editor */}}
<form
autocomplete="off"
class="w-full dbform"
id="abkform"
method="POST"
action="/abkuerzungen/">
<input type="hidden" name="csrf_token" value="{{ $model.csrf_token }}" />
<div class="mb-4 flex items-center gap-3">
<button type="button" id="add-row-btn" class="resetbutton w-auto px-3 py-1.5 flex items-center gap-2">
<i class="ri-add-line"></i>
<span>Zeile hinzufügen</span>
</button>
</div>
<div class="overflow-x-auto bg-white border border-gray-200 rounded">
<table class="w-full">
<thead>
<tr class="border-b-2 border-gray-300 bg-gray-50">
<th class="text-left px-4 py-3 font-bold text-sm text-gray-700 w-48">Kürzel</th>
<th class="text-left px-4 py-3 font-bold text-sm text-gray-700">Bedeutung</th>
<th class="w-16 px-4 py-3"></th>
</tr>
</thead>
<tbody id="abk-tbody">
{{- range $index, $entry := $model.result.Entries -}}
<tr class="border-b border-gray-200 hover:bg-gray-50 odd:bg-stone-100" data-row="{{ $index }}">
<td class="px-4 py-2.5">
<input type="text"
name="abkuerzungen[{{ $index }}][key]"
value="{{ $entry.Key }}"
class="inputinput font-mono text-base w-full border border-gray-300 px-2 py-1 rounded"
autocomplete="off"
required>
</td>
<td class="px-4 py-2.5">
<input type="text"
name="abkuerzungen[{{ $index }}][value]"
value="{{ $entry.Value }}"
class="inputinput text-base w-full border border-gray-300 px-2 py-1 rounded"
autocomplete="off"
required>
</td>
<td class="px-4 py-2.5 text-center">
<input type="hidden" name="abkuerzungen[{{ $index }}][_delete]" value="false" class="delete-flag">
<button type="button" class="delete-btn text-red-600 hover:text-red-800" title="Löschen">
<i class="ri-delete-bin-line text-lg"></i>
</button>
</td>
</tr>
{{- end -}}
</tbody>
</table>
</div>
<div class="w-full flex items-end justify-between gap-4 mt-6 flex-wrap">
<p id="abk-save-feedback" class="text-sm text-gray-600" aria-live="polite"></p>
<div class="flex items-center gap-3 self-end flex-wrap">
<a href="/abkuerzungen/" class="resetbutton w-40 flex items-center gap-2 justify-center">
<i class="ri-close-line"></i>
<span>Abbrechen</span>
</a>
<a href="/abkuerzungen/" class="resetbutton w-40 flex items-center gap-2 justify-center">
<i class="ri-loop-left-line"></i>
<span>Reset</span>
</a>
<button type="submit" class="submitbutton w-48 flex items-center gap-2 justify-center">
<i class="ri-save-line"></i>
<span>Alle speichern</span>
</button>
</div>
</div>
</form>
<script>
(function() {
const tbody = document.getElementById('abk-tbody');
const addBtn = document.getElementById('add-row-btn');
let rowIndex = {{ len $model.result.Entries }};
// Add new row
addBtn.addEventListener('click', function() {
const tr = document.createElement('tr');
tr.className = 'border-b border-gray-200 hover:bg-gray-50 odd:bg-stone-100';
tr.dataset.row = rowIndex;
tr.innerHTML = `
<td class="px-4 py-2.5">
<input type="text"
name="abkuerzungen[${rowIndex}][key]"
value=""
class="inputinput font-mono text-base w-full border border-gray-300 px-2 py-1 rounded"
autocomplete="off"
required>
</td>
<td class="px-4 py-2.5">
<input type="text"
name="abkuerzungen[${rowIndex}][value]"
value=""
class="inputinput text-base w-full border border-gray-300 px-2 py-1 rounded"
autocomplete="off"
required>
</td>
<td class="px-4 py-2.5 text-center">
<input type="hidden" name="abkuerzungen[${rowIndex}][_delete]" value="false" class="delete-flag">
<button type="button" class="delete-btn text-red-600 hover:text-red-800" title="Löschen">
<i class="ri-delete-bin-line text-lg"></i>
</button>
</td>
`;
tbody.appendChild(tr);
rowIndex++;
// Focus the new key input
const newKeyInput = tr.querySelector('input[type="text"]');
if (newKeyInput) {
newKeyInput.focus();
}
});
// Delete row handler (using event delegation)
tbody.addEventListener('click', function(e) {
if (e.target.classList.contains('delete-btn')) {
const tr = e.target.closest('tr');
const deleteFlag = tr.querySelector('.delete-flag');
deleteFlag.value = 'true';
tr.style.display = 'none';
}
});
})();
</script>
{{- else -}}
{{/* Read-only view for public */}}
<div class="bg-white border border-gray-200 rounded overflow-hidden">
{{- if $model.result.Entries -}}
<table class="w-full">
<thead>
<tr class="border-b-2 border-gray-300 bg-gray-50">
<th class="text-left px-4 py-3 font-bold text-sm text-gray-700 w-48">Kürzel</th>
<th class="text-left px-4 py-3 font-bold text-sm text-gray-700">Bedeutung</th>
</tr>
</thead>
<tbody>
{{- range $entry := $model.result.Entries -}}
<tr class="border-b border-gray-200 hover:bg-gray-50 odd:bg-stone-100">
<td class="px-4 py-2.5 font-bold font-mono text-base">{{ $entry.Key }}</td>
<td class="px-4 py-2.5 text-base">{{ $entry.Value }}</td>
</tr>
{{- end -}}
</tbody>
</table>
{{- else -}}
<div class="px-4 py-8 text-center text-gray-500">Keine Abkürzungen gefunden.</div>
{{- end -}}
</div>
{{- end -}}
</div>
</edit-page>

View File

@@ -187,7 +187,7 @@
</div> </div>
<script type="module"> <script type="module">
let abbrevs = {{- $model.abbrs -}}; let abbrevs = {{- data "abkuerzungen" | SafeJS -}};
let ats = document.querySelectorAll('abbrev-tooltips'); let ats = document.querySelectorAll('abbrev-tooltips');
if (ats) { if (ats) {
ats.forEach((at) => { ats.forEach((at) => {

View File

@@ -179,6 +179,15 @@ export class FabMenu extends HTMLElement {
<i class="ri-external-link-line text-base"></i> <i class="ri-external-link-line text-base"></i>
</a> </a>
</div> </div>
<div class="grid grid-cols-[1fr_auto] group">
<a href="/abkuerzungen/" class="flex items-center px-4 py-2 group-hover:bg-gray-100 transition-colors no-underline text-sm">
<i class="ri-text text-base text-gray-700 mr-2.5"></i>
<span class="text-gray-900">Abkürzungen</span>
</a>
<a href="/abkuerzungen/" target="_blank" class="flex items-center justify-center px-3 py-2 group-hover:bg-gray-100 text-gray-700 hover:text-slate-900 transition-colors no-underline text-sm" title="In neuem Tab öffnen">
<i class="ri-external-link-line text-base"></i>
</a>
</div>
<div class="border-t border-gray-200 my-1"></div> <div class="border-t border-gray-200 my-1"></div>
` `
: ""; : "";