mirror of
https://github.com/Theodor-Springmann-Stiftung/musenalm.git
synced 2026-02-04 02:25:30 +00:00
+Abkürzungen
This commit is contained in:
@@ -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
210
controllers/abkuerzungen.go
Normal 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)
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
@@ -84,7 +79,7 @@ type AlmanachEditResult struct {
|
|||||||
PrevByID *dbmodels.Entry
|
PrevByID *dbmodels.Entry
|
||||||
NextByTitle *dbmodels.Entry
|
NextByTitle *dbmodels.Entry
|
||||||
PrevByTitle *dbmodels.Entry
|
PrevByTitle *dbmodels.Entry
|
||||||
User *dbmodels.User
|
User *dbmodels.User
|
||||||
AlmanachResult
|
AlmanachResult
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -165,7 +160,7 @@ func (p *AlmanachNewPage) POSTSave(engine *templating.Engine, app core.App) Hand
|
|||||||
|
|
||||||
redirect := "/"
|
redirect := "/"
|
||||||
if entry != nil {
|
if entry != nil {
|
||||||
redirect = "/almanach/" + strconv.Itoa(entry.MusenalmID()) + "/edit?saved_message=" + url.QueryEscape("Änderungen gespeichert.")
|
redirect = "/almanach/" + strconv.Itoa(entry.MusenalmID()) + "/edit?saved_message=" + url.QueryEscape("Änderungen gespeichert.")
|
||||||
}
|
}
|
||||||
|
|
||||||
return e.JSON(http.StatusOK, map[string]any{
|
return e.JSON(http.StatusOK, map[string]any{
|
||||||
|
|||||||
@@ -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)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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](
|
||||||
|
|||||||
@@ -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>")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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"
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
186
views/routes/abkuerzungen/body.gohtml
Normal file
186
views/routes/abkuerzungen/body.gohtml
Normal 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>
|
||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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>
|
||||||
`
|
`
|
||||||
: "";
|
: "";
|
||||||
|
|||||||
Reference in New Issue
Block a user