mirror of
https://github.com/Theodor-Springmann-Stiftung/kgpz_web.git
synced 2025-10-28 08:35:30 +00:00
Some serious layout changes
This commit is contained in:
@@ -38,7 +38,7 @@ const (
|
||||
YEAR_OVERVIEW_URL = "/jahrgang/:year"
|
||||
PLACE_OVERVIEW_URL = "/ort/:place?"
|
||||
AGENTS_OVERVIEW_URL = "/akteure/:letterorid"
|
||||
CATEGORY_OVERVIEW_URL = "/kategorie/:category"
|
||||
CATEGORY_OVERVIEW_URL = "/kategorie/:category?/:year?"
|
||||
|
||||
PIECE_URL = "/beitrag/:id"
|
||||
PIECE_PAGE_URL = "/beitrag/:id/:page"
|
||||
|
||||
@@ -140,12 +140,80 @@ func GetQuickFilter(kgpz *xmlmodels.Library) fiber.Handler {
|
||||
return strings.Compare(a.ID, b.ID)
|
||||
})
|
||||
|
||||
// Get all categories with their first available year
|
||||
categories := make([]CategorySummary, 0)
|
||||
categoryYears := make(map[string][]int) // categoryID -> list of years
|
||||
|
||||
// First pass: collect all years for each category
|
||||
for _, piece := range kgpz.Pieces.Array {
|
||||
// Get years for this piece
|
||||
pieceYears := make(map[int]bool)
|
||||
for _, issueRef := range piece.IssueRefs {
|
||||
if issueRef.When.Year > 0 {
|
||||
pieceYears[issueRef.When.Year] = true
|
||||
}
|
||||
}
|
||||
|
||||
// Process CategoryRefs
|
||||
for _, catRef := range piece.CategoryRefs {
|
||||
if catRef.Ref != "" {
|
||||
for year := range pieceYears {
|
||||
categoryYears[catRef.Ref] = append(categoryYears[catRef.Ref], year)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process WorkRefs with categories (rezension)
|
||||
for _, workRef := range piece.WorkRefs {
|
||||
categoryID := workRef.Category
|
||||
if categoryID == "" {
|
||||
categoryID = "rezension" // Default category for WorkRefs
|
||||
}
|
||||
for year := range pieceYears {
|
||||
categoryYears[categoryID] = append(categoryYears[categoryID], year)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Build categories list with first year for each
|
||||
kgpz.Categories.Lock()
|
||||
for _, category := range kgpz.Categories.Array {
|
||||
if yearsList, exists := categoryYears[category.ID]; exists && len(yearsList) > 0 {
|
||||
// Find the earliest year for this category
|
||||
slices.Sort(yearsList)
|
||||
firstYear := yearsList[0]
|
||||
|
||||
// Get the primary name (first name in the list)
|
||||
var name string
|
||||
if len(category.Names) > 0 {
|
||||
name = category.Names[0]
|
||||
} else {
|
||||
name = category.ID // fallback to ID if no names
|
||||
}
|
||||
|
||||
categorySummary := CategorySummary{
|
||||
ID: category.ID,
|
||||
Name: name,
|
||||
FirstYear: firstYear,
|
||||
}
|
||||
|
||||
categories = append(categories, categorySummary)
|
||||
}
|
||||
}
|
||||
kgpz.Categories.Unlock()
|
||||
|
||||
// Sort categories list by name
|
||||
slices.SortFunc(categories, func(a, b CategorySummary) int {
|
||||
return strings.Compare(a.Name, b.Name)
|
||||
})
|
||||
|
||||
// Prepare data for the filter template
|
||||
data := fiber.Map{
|
||||
"AvailableYears": availableYears,
|
||||
"Persons": persons,
|
||||
"Authors": authors,
|
||||
"Places": places,
|
||||
"Categories": categories,
|
||||
"IssuesByYearJSON": string(issuesByYearJSON),
|
||||
}
|
||||
|
||||
@@ -168,6 +236,13 @@ type PlaceSummary struct {
|
||||
Geo string
|
||||
}
|
||||
|
||||
// CategorySummary represents a simplified category for the filter list
|
||||
type CategorySummary struct {
|
||||
ID string
|
||||
Name string
|
||||
FirstYear int
|
||||
}
|
||||
|
||||
// IssueSummary represents an issue for the Jahr/Ausgabe filter
|
||||
type IssueSummary struct {
|
||||
Number int `json:"number"`
|
||||
|
||||
@@ -1,12 +1,111 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/Theodor-Springmann-Stiftung/kgpz_web/helpers/logging"
|
||||
"github.com/Theodor-Springmann-Stiftung/kgpz_web/viewmodels"
|
||||
"github.com/Theodor-Springmann-Stiftung/kgpz_web/xmlmodels"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
const (
|
||||
DEFAULT_CATEGORY = ""
|
||||
)
|
||||
|
||||
func GetCategory(kgpz *xmlmodels.Library) fiber.Handler {
|
||||
return func(c *fiber.Ctx) error {
|
||||
return c.Render("/kategorie/", nil)
|
||||
categoryID := c.Params("category", DEFAULT_CATEGORY)
|
||||
categoryID = strings.ToLower(categoryID)
|
||||
yearParam := c.Params("year", "")
|
||||
|
||||
// Case 1: No category specified - show categories list with years
|
||||
if categoryID == "" {
|
||||
categories := viewmodels.CategoriesView(kgpz)
|
||||
if len(categories.Categories) == 0 {
|
||||
logging.Error(nil, "No categories found")
|
||||
return c.SendStatus(fiber.StatusNotFound)
|
||||
}
|
||||
return c.Render("/kategorie/list/", fiber.Map{
|
||||
"model": categories,
|
||||
})
|
||||
}
|
||||
|
||||
// Case 2: Both category and year specified - show pieces
|
||||
if yearParam != "" {
|
||||
year, err := strconv.Atoi(yearParam)
|
||||
if err != nil {
|
||||
logging.Error(err, "Invalid year parameter: "+yearParam)
|
||||
return c.SendStatus(fiber.StatusBadRequest)
|
||||
}
|
||||
|
||||
categoryPieces := viewmodels.NewCategoryPiecesView(categoryID, year, kgpz)
|
||||
if categoryPieces == nil {
|
||||
logging.Error(nil, "Category not found: "+categoryID)
|
||||
return c.SendStatus(fiber.StatusNotFound)
|
||||
}
|
||||
|
||||
return c.Render("/kategorie/pieces/", fiber.Map{
|
||||
"model": categoryPieces,
|
||||
})
|
||||
}
|
||||
|
||||
// Case 3: Category specified but no year - find first available year and redirect
|
||||
firstYear := findFirstYearForCategory(categoryID, kgpz)
|
||||
if firstYear == 0 {
|
||||
// Category exists but has no pieces, redirect to category list
|
||||
logging.Error(nil, "Category has no pieces: "+categoryID)
|
||||
return c.Redirect("/kategorie/")
|
||||
}
|
||||
|
||||
// Redirect to category with first available year
|
||||
return c.Redirect("/kategorie/" + categoryID + "/" + strconv.Itoa(firstYear))
|
||||
}
|
||||
}
|
||||
|
||||
// findFirstYearForCategory finds the earliest year that has pieces for the given category
|
||||
func findFirstYearForCategory(categoryID string, kgpz *xmlmodels.Library) int {
|
||||
categoryYears := make([]int, 0)
|
||||
|
||||
for _, piece := range kgpz.Pieces.Array {
|
||||
matchesCategory := false
|
||||
|
||||
// Check direct CategoryRefs
|
||||
for _, catRef := range piece.CategoryRefs {
|
||||
if catRef.Ref == categoryID {
|
||||
matchesCategory = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Check WorkRefs with categories if not found in direct refs
|
||||
if !matchesCategory {
|
||||
for _, workRef := range piece.WorkRefs {
|
||||
if workRef.Category == categoryID || (workRef.Category == "" && categoryID == "rezension") {
|
||||
matchesCategory = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if matchesCategory {
|
||||
// Extract years from IssueRefs
|
||||
for _, issueRef := range piece.IssueRefs {
|
||||
year := issueRef.When.Year
|
||||
if year > 0 {
|
||||
categoryYears = append(categoryYears, year)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(categoryYears) == 0 {
|
||||
return 0 // No pieces found for this category
|
||||
}
|
||||
|
||||
// Find the earliest year
|
||||
slices.Sort(categoryYears)
|
||||
return categoryYears[0]
|
||||
}
|
||||
|
||||
291
viewmodels/category_view.go
Normal file
291
viewmodels/category_view.go
Normal file
@@ -0,0 +1,291 @@
|
||||
package viewmodels
|
||||
|
||||
import (
|
||||
"slices"
|
||||
|
||||
"github.com/Theodor-Springmann-Stiftung/kgpz_web/xmlmodels"
|
||||
)
|
||||
|
||||
// CategoriesListView represents the data for the categories overview page
|
||||
type CategoriesListView struct {
|
||||
Categories map[string]CategoryWithPieceCount
|
||||
Sorted []string
|
||||
}
|
||||
|
||||
// CategoryDetailView represents a specific category with its available years
|
||||
type CategoryDetailView struct {
|
||||
Category xmlmodels.Category
|
||||
CategoryReadable map[string]interface{}
|
||||
AvailableYears []YearWithPieceCount
|
||||
PieceCount int
|
||||
}
|
||||
|
||||
// CategoryPiecesView represents pieces filtered by category and year
|
||||
type CategoryPiecesView struct {
|
||||
Category xmlmodels.Category
|
||||
CategoryReadable map[string]interface{}
|
||||
Year int
|
||||
Pieces []xmlmodels.Piece
|
||||
PieceCount int
|
||||
AvailableYears []YearWithPieceCount
|
||||
}
|
||||
|
||||
// CategoryWithPieceCount represents a category with total piece count and available years
|
||||
type CategoryWithPieceCount struct {
|
||||
Category xmlmodels.Category
|
||||
Readable map[string]interface{}
|
||||
PieceCount int
|
||||
AvailableYears []YearWithPieceCount
|
||||
}
|
||||
|
||||
// YearWithPieceCount represents a year with piece count for a specific category
|
||||
type YearWithPieceCount struct {
|
||||
Year int
|
||||
PieceCount int
|
||||
}
|
||||
|
||||
// CategoriesView returns categories data for the overview page
|
||||
func CategoriesView(lib *xmlmodels.Library) *CategoriesListView {
|
||||
res := CategoriesListView{Categories: make(map[string]CategoryWithPieceCount)}
|
||||
|
||||
// Count pieces per category and year
|
||||
categoryPieceCounts := make(map[string]int)
|
||||
categoryYearCounts := make(map[string]map[int]int) // categoryID -> year -> count
|
||||
|
||||
for _, piece := range lib.Pieces.Array {
|
||||
// Get years for this piece
|
||||
pieceYears := make(map[int]bool)
|
||||
for _, issueRef := range piece.IssueRefs {
|
||||
if issueRef.When.Year > 0 {
|
||||
pieceYears[issueRef.When.Year] = true
|
||||
}
|
||||
}
|
||||
|
||||
// Process CategoryRefs
|
||||
for _, catRef := range piece.CategoryRefs {
|
||||
if catRef.Ref != "" {
|
||||
categoryPieceCounts[catRef.Ref]++
|
||||
if categoryYearCounts[catRef.Ref] == nil {
|
||||
categoryYearCounts[catRef.Ref] = make(map[int]int)
|
||||
}
|
||||
for year := range pieceYears {
|
||||
categoryYearCounts[catRef.Ref][year]++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process WorkRefs with categories
|
||||
for _, workRef := range piece.WorkRefs {
|
||||
categoryID := workRef.Category
|
||||
if categoryID == "" {
|
||||
categoryID = "rezension" // Default category for WorkRefs
|
||||
}
|
||||
categoryPieceCounts[categoryID]++
|
||||
if categoryYearCounts[categoryID] == nil {
|
||||
categoryYearCounts[categoryID] = make(map[int]int)
|
||||
}
|
||||
for year := range pieceYears {
|
||||
categoryYearCounts[categoryID][year]++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Build categories list with piece counts and available years
|
||||
for _, category := range lib.Categories.Array {
|
||||
if categoryPieceCounts[category.ID] > 0 {
|
||||
// Merge readable and readable HTML data
|
||||
readable := category.Readable(lib)
|
||||
readableHTML := category.ReadableHTML()
|
||||
for k, v := range readableHTML {
|
||||
readable[k] = v
|
||||
}
|
||||
|
||||
// Build available years list for this category
|
||||
var availableYears []YearWithPieceCount
|
||||
if yearCounts, exists := categoryYearCounts[category.ID]; exists {
|
||||
years := make([]int, 0, len(yearCounts))
|
||||
for year := range yearCounts {
|
||||
years = append(years, year)
|
||||
}
|
||||
slices.Sort(years)
|
||||
|
||||
for _, year := range years {
|
||||
availableYears = append(availableYears, YearWithPieceCount{
|
||||
Year: year,
|
||||
PieceCount: yearCounts[year],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
res.Categories[category.ID] = CategoryWithPieceCount{
|
||||
Category: category,
|
||||
Readable: readable,
|
||||
PieceCount: categoryPieceCounts[category.ID],
|
||||
AvailableYears: availableYears,
|
||||
}
|
||||
res.Sorted = append(res.Sorted, category.ID)
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by category ID
|
||||
slices.Sort(res.Sorted)
|
||||
|
||||
return &res
|
||||
}
|
||||
|
||||
// CategoryDetailView returns category data with available years
|
||||
func CategoryView(categoryID string, lib *xmlmodels.Library) *CategoryDetailView {
|
||||
// Get the category
|
||||
category := lib.Categories.Item(categoryID)
|
||||
if category == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Count pieces per year for this category
|
||||
yearPieceCounts := make(map[int]int)
|
||||
totalPieces := 0
|
||||
|
||||
for _, piece := range lib.Pieces.Array {
|
||||
matchesCategory := false
|
||||
|
||||
// Check direct CategoryRefs
|
||||
for _, catRef := range piece.CategoryRefs {
|
||||
if catRef.Ref == categoryID {
|
||||
matchesCategory = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Check WorkRefs with categories if not found in direct refs
|
||||
if !matchesCategory {
|
||||
for _, workRef := range piece.WorkRefs {
|
||||
if workRef.Category == categoryID || (workRef.Category == "" && categoryID == "rezension") {
|
||||
matchesCategory = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if matchesCategory {
|
||||
totalPieces++
|
||||
// Extract years from IssueRefs
|
||||
for _, issueRef := range piece.IssueRefs {
|
||||
year := issueRef.When.Year
|
||||
if year > 0 {
|
||||
yearPieceCounts[year]++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Build available years list
|
||||
var availableYears []YearWithPieceCount
|
||||
years := make([]int, 0, len(yearPieceCounts))
|
||||
for year := range yearPieceCounts {
|
||||
years = append(years, year)
|
||||
}
|
||||
slices.Sort(years)
|
||||
|
||||
for _, year := range years {
|
||||
availableYears = append(availableYears, YearWithPieceCount{
|
||||
Year: year,
|
||||
PieceCount: yearPieceCounts[year],
|
||||
})
|
||||
}
|
||||
|
||||
// Merge readable and readable HTML data
|
||||
readable := category.Readable(lib)
|
||||
readableHTML := category.ReadableHTML()
|
||||
for k, v := range readableHTML {
|
||||
readable[k] = v
|
||||
}
|
||||
|
||||
return &CategoryDetailView{
|
||||
Category: *category,
|
||||
CategoryReadable: readable,
|
||||
AvailableYears: availableYears,
|
||||
PieceCount: totalPieces,
|
||||
}
|
||||
}
|
||||
|
||||
// NewCategoryPiecesView returns pieces filtered by category and year
|
||||
func NewCategoryPiecesView(categoryID string, year int, lib *xmlmodels.Library) *CategoryPiecesView {
|
||||
// Get the category
|
||||
category := lib.Categories.Item(categoryID)
|
||||
if category == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var pieces []xmlmodels.Piece
|
||||
yearPieceCounts := make(map[int]int)
|
||||
|
||||
for _, piece := range lib.Pieces.Array {
|
||||
matchesCategory := false
|
||||
|
||||
// Check direct CategoryRefs
|
||||
for _, catRef := range piece.CategoryRefs {
|
||||
if catRef.Ref == categoryID {
|
||||
matchesCategory = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Check WorkRefs with categories if not found in direct refs
|
||||
if !matchesCategory {
|
||||
for _, workRef := range piece.WorkRefs {
|
||||
if workRef.Category == categoryID || (workRef.Category == "" && categoryID == "rezension") {
|
||||
matchesCategory = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if matchesCategory {
|
||||
// Count all years for this category (for navigation)
|
||||
for _, issueRef := range piece.IssueRefs {
|
||||
if issueRef.When.Year > 0 {
|
||||
yearPieceCounts[issueRef.When.Year]++
|
||||
}
|
||||
}
|
||||
|
||||
// Check if piece appears in the specified year
|
||||
for _, issueRef := range piece.IssueRefs {
|
||||
if issueRef.When.Year == year {
|
||||
pieces = append(pieces, piece)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Build available years list
|
||||
var availableYears []YearWithPieceCount
|
||||
years := make([]int, 0, len(yearPieceCounts))
|
||||
for yearVal := range yearPieceCounts {
|
||||
years = append(years, yearVal)
|
||||
}
|
||||
slices.Sort(years)
|
||||
|
||||
for _, yearVal := range years {
|
||||
availableYears = append(availableYears, YearWithPieceCount{
|
||||
Year: yearVal,
|
||||
PieceCount: yearPieceCounts[yearVal],
|
||||
})
|
||||
}
|
||||
|
||||
// Merge readable and readable HTML data
|
||||
readable := category.Readable(lib)
|
||||
readableHTML := category.ReadableHTML()
|
||||
for k, v := range readableHTML {
|
||||
readable[k] = v
|
||||
}
|
||||
|
||||
return &CategoryPiecesView{
|
||||
Category: *category,
|
||||
CategoryReadable: readable,
|
||||
Year: year,
|
||||
Pieces: pieces,
|
||||
PieceCount: len(pieces),
|
||||
AvailableYears: availableYears,
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ type PlacesListView struct {
|
||||
Places map[string]xmlmodels.Place
|
||||
Sorted []string
|
||||
SelectedPlace *PlaceDetailView
|
||||
TotalPiecesWithPlaces int
|
||||
}
|
||||
|
||||
// PlaceDetailView represents a specific place with its associated pieces
|
||||
@@ -28,11 +29,18 @@ func PlacesView(placeID string, lib *xmlmodels.Library) *PlacesListView {
|
||||
res := PlacesListView{Search: placeID, Places: make(map[string]xmlmodels.Place)}
|
||||
av := make(map[string]bool)
|
||||
|
||||
// Get all places that are referenced in pieces
|
||||
// Get all places that are referenced in pieces and count total pieces with places
|
||||
referencedPlaces := make(map[string]bool)
|
||||
totalPiecesWithPlaces := 0
|
||||
|
||||
for _, piece := range lib.Pieces.Array {
|
||||
hasPlace := false
|
||||
for _, placeRef := range piece.PlaceRefs {
|
||||
referencedPlaces[placeRef.Ref] = true
|
||||
hasPlace = true
|
||||
}
|
||||
if hasPlace {
|
||||
totalPiecesWithPlaces++
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,6 +64,7 @@ func PlacesView(placeID string, lib *xmlmodels.Library) *PlacesListView {
|
||||
res.AvailableLetters = slices.Collect(maps.Keys(av))
|
||||
slices.Sort(res.AvailableLetters)
|
||||
slices.Sort(res.Sorted)
|
||||
res.TotalPiecesWithPlaces = totalPiecesWithPlaces
|
||||
|
||||
return &res
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ document.body.addEventListener("htmx:configRequest", function(a) {
|
||||
let e = a.detail.elt;
|
||||
e.id === "search" && e.value === "" && (a.detail.parameters = {}, a.detail.path = window.location.pathname + window.location.search);
|
||||
});
|
||||
class H extends HTMLElement {
|
||||
class A extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
@@ -35,34 +35,36 @@ class H extends HTMLElement {
|
||||
});
|
||||
}
|
||||
}
|
||||
customElements.define("person-jump-filter", H);
|
||||
class B extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
customElements.define("person-jump-filter", A);
|
||||
class H extends HTMLElement {
|
||||
connectedCallback() {
|
||||
this.setupEventListeners();
|
||||
}
|
||||
setupEventListeners() {
|
||||
const e = this.querySelector("#place-search"), t = this.querySelector("#all-places");
|
||||
!e || !t || e.addEventListener("input", (i) => {
|
||||
const n = i.target.value.toLowerCase().trim();
|
||||
this.filterPlaces(n);
|
||||
});
|
||||
}
|
||||
filterPlaces(e) {
|
||||
const t = this.querySelector("#all-places");
|
||||
if (!t)
|
||||
return;
|
||||
t.querySelectorAll(".place-item").forEach((n) => {
|
||||
var r;
|
||||
const s = ((r = n.querySelector(".place-name")) == null ? void 0 : r.textContent) || "";
|
||||
!e || s.toLowerCase().includes(e) ? n.style.display = "block" : n.style.display = "none";
|
||||
const e = this.querySelector("#place-search");
|
||||
e && e.addEventListener("input", (t) => {
|
||||
const i = t.target.value.toLowerCase().trim();
|
||||
this.querySelectorAll(".place-item").forEach((s) => {
|
||||
var l;
|
||||
const o = ((l = s.querySelector(".place-name")) == null ? void 0 : l.textContent) || "", r = !i || o.toLowerCase().includes(i);
|
||||
s.style.display = r ? "block" : "none";
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
customElements.define("place-jump-filter", B);
|
||||
class A extends HTMLElement {
|
||||
customElements.define("place-jump-filter", H);
|
||||
class B extends HTMLElement {
|
||||
connectedCallback() {
|
||||
const e = this.querySelector("#category-search");
|
||||
e && e.addEventListener("input", (t) => {
|
||||
const i = t.target.value.toLowerCase().trim();
|
||||
this.querySelectorAll(".category-item").forEach((s) => {
|
||||
var l;
|
||||
const o = ((l = s.querySelector(".category-name")) == null ? void 0 : l.textContent) || "", r = !i || o.toLowerCase().includes(i);
|
||||
s.style.display = r ? "block" : "none";
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
customElements.define("category-jump-filter", B);
|
||||
class M extends HTMLElement {
|
||||
constructor() {
|
||||
super(), this.issuesByYear = {};
|
||||
}
|
||||
@@ -177,8 +179,8 @@ class A extends HTMLElement {
|
||||
i.disabled = !o;
|
||||
}
|
||||
}
|
||||
customElements.define("year-jump-filter", A);
|
||||
class M extends HTMLElement {
|
||||
customElements.define("year-jump-filter", M);
|
||||
class N extends HTMLElement {
|
||||
constructor() {
|
||||
super(), this.isOpen = !1;
|
||||
}
|
||||
@@ -240,8 +242,8 @@ class M extends HTMLElement {
|
||||
this.isOpen && t && i && !t.contains(e.target) && !this.contains(e.target) && this.hideFilter();
|
||||
}
|
||||
}
|
||||
customElements.define("schnellauswahl-button", M);
|
||||
class N extends HTMLElement {
|
||||
customElements.define("schnellauswahl-button", N);
|
||||
class $ extends HTMLElement {
|
||||
constructor() {
|
||||
super(), this.isOpen = !1;
|
||||
}
|
||||
@@ -264,7 +266,7 @@ class N extends HTMLElement {
|
||||
<div>Übersicht nach</div>
|
||||
<div class="ml-2 flex flex-col gap-y-2 mt-2">
|
||||
<a href="/">Jahrgängen</a>
|
||||
<a href="/akteure/a">Personen</a>
|
||||
<a href="/akteure/a">Personen & Werke</a>
|
||||
<a href="/kategorie/">Betragsarten</a>
|
||||
<a href="/ort/">Orten</a>
|
||||
</div>
|
||||
@@ -317,7 +319,7 @@ class N extends HTMLElement {
|
||||
this.isOpen && !this.contains(e.target) && this.hideMenu();
|
||||
}
|
||||
}
|
||||
customElements.define("navigation-menu", N);
|
||||
customElements.define("navigation-menu", $);
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
document.addEventListener("click", function(a) {
|
||||
const e = a.target.closest('a[href^="/akteure/"], a[href^="/ort/"]'), t = document.getElementById("filter-container");
|
||||
@@ -490,9 +492,9 @@ class O extends HTMLElement {
|
||||
document.documentElement.offsetHeight
|
||||
), s = window.innerHeight, o = n - s, r = o > 0 ? window.scrollY / o : 0, l = t.clientHeight, d = t.scrollHeight - l;
|
||||
if (d > 0) {
|
||||
const u = r * d, h = i.getBoundingClientRect(), p = t.getBoundingClientRect(), f = h.top - p.top + t.scrollTop, m = l / 2, I = f - m, v = 0.7, T = v * u + (1 - v) * I, y = Math.max(0, Math.min(d, T)), q = t.scrollTop;
|
||||
Math.abs(y - q) > 10 && t.scrollTo({
|
||||
top: y,
|
||||
const u = r * d, h = i.getBoundingClientRect(), p = t.getBoundingClientRect(), f = h.top - p.top + t.scrollTop, m = l / 2, T = f - m, y = 0.7, I = y * u + (1 - y) * T, v = Math.max(0, Math.min(d, I)), q = t.scrollTop;
|
||||
Math.abs(v - q) > 10 && t.scrollTo({
|
||||
top: v,
|
||||
behavior: "smooth"
|
||||
});
|
||||
}
|
||||
@@ -512,7 +514,7 @@ class O extends HTMLElement {
|
||||
}
|
||||
}
|
||||
customElements.define("akteure-scrollspy", O);
|
||||
class $ extends HTMLElement {
|
||||
class V extends HTMLElement {
|
||||
constructor() {
|
||||
super(), this.searchInput = null, this.placeCards = [], this.countElement = null, this.debounceTimer = null, this.originalCount = 0;
|
||||
}
|
||||
@@ -564,8 +566,82 @@ class $ extends HTMLElement {
|
||||
this.countElement && (t === "" ? this.countElement.textContent = `Alle Orte (${this.originalCount})` : e === 0 ? this.countElement.textContent = `Keine Orte gefunden für "${t}"` : this.countElement.textContent = `${e} von ${this.originalCount} Orten`);
|
||||
}
|
||||
}
|
||||
customElements.define("places-filter", $);
|
||||
class V extends HTMLElement {
|
||||
customElements.define("places-filter", V);
|
||||
class R extends HTMLElement {
|
||||
constructor() {
|
||||
super(), this.searchInput = null, this.itemCards = [], this.countElement = null, this.debounceTimer = null, this.originalCount = 0;
|
||||
}
|
||||
connectedCallback() {
|
||||
this.placeholderText = this.getAttribute("placeholder") || "Suchen...", this.itemSelector = this.getAttribute("item-selector") || "[data-filter-item]", this.searchAttributes = (this.getAttribute("search-attributes") || "data-filter-text").split(","), this.countSelector = this.getAttribute("count-selector") || "[data-filter-count]", this.itemType = this.getAttribute("item-type") || "Einträge", this.itemTypeSingular = this.getAttribute("item-type-singular") || "Eintrag", this.render(), this.setupEventListeners(), this.initializeItems();
|
||||
}
|
||||
disconnectedCallback() {
|
||||
this.cleanupEventListeners(), this.debounceTimer && clearTimeout(this.debounceTimer);
|
||||
}
|
||||
render() {
|
||||
this.innerHTML = `
|
||||
<div class="mb-6">
|
||||
<input
|
||||
type="text"
|
||||
id="generic-search"
|
||||
placeholder="${this.placeholderText}"
|
||||
autocomplete="off"
|
||||
class="w-full px-3 py-2 border border-slate-300 rounded-md text-sm bg-white focus:outline-none focus:ring-1 focus:ring-blue-400 focus:border-blue-400"
|
||||
>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
setupEventListeners() {
|
||||
this.searchInput = this.querySelector("#generic-search"), this.searchInput && this.searchInput.addEventListener("input", this.handleSearchInput.bind(this));
|
||||
}
|
||||
cleanupEventListeners() {
|
||||
this.searchInput && this.searchInput.removeEventListener("input", this.handleSearchInput.bind(this));
|
||||
}
|
||||
initializeItems() {
|
||||
this.itemCards = Array.from(document.querySelectorAll(this.itemSelector));
|
||||
const e = this.closest(".filter-sidebar") || this.closest(".sidebar") || document;
|
||||
this.countElement = e.querySelector(this.countSelector), console.log("GenericFilter initialized:", {
|
||||
itemSelector: this.itemSelector,
|
||||
itemsFound: this.itemCards.length,
|
||||
countElement: this.countElement,
|
||||
searchAttributes: this.searchAttributes
|
||||
}), this.countElement && (this.originalCount = this.itemCards.length);
|
||||
}
|
||||
handleSearchInput(e) {
|
||||
this.debounceTimer && clearTimeout(this.debounceTimer), this.debounceTimer = setTimeout(() => {
|
||||
this.filterItems(e.target.value.trim());
|
||||
}, 150);
|
||||
}
|
||||
filterItems(e) {
|
||||
if (!this.itemCards.length) return;
|
||||
const t = e.toLowerCase();
|
||||
let i = 0;
|
||||
this.itemCards.forEach((n) => {
|
||||
var o;
|
||||
let s = e === "";
|
||||
if (!s) {
|
||||
for (const r of this.searchAttributes)
|
||||
if ((((o = n.getAttribute(r.trim())) == null ? void 0 : o.toLowerCase()) || "").includes(t)) {
|
||||
s = !0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
s ? (n.style.display = "", i++) : n.style.display = "none";
|
||||
}), this.updateCountDisplay(i, e);
|
||||
}
|
||||
updateCountDisplay(e, t) {
|
||||
if (this.countElement)
|
||||
if (t === "")
|
||||
this.countElement.textContent = `Alle ${this.itemType} (${this.originalCount})`;
|
||||
else if (e === 0)
|
||||
this.countElement.textContent = `Keine ${this.itemType} gefunden für "${t}"`;
|
||||
else {
|
||||
const i = e === 1 ? this.itemTypeSingular : this.itemType;
|
||||
this.countElement.textContent = `${e} von ${this.originalCount} ${i}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
customElements.define("generic-filter", R);
|
||||
class z extends HTMLElement {
|
||||
constructor() {
|
||||
super(), this.resizeObserver = null;
|
||||
}
|
||||
@@ -918,7 +994,7 @@ class V extends HTMLElement {
|
||||
return "KGPZ";
|
||||
}
|
||||
}
|
||||
customElements.define("single-page-viewer", V);
|
||||
customElements.define("single-page-viewer", z);
|
||||
document.body.addEventListener("htmx:beforeRequest", function(a) {
|
||||
const e = document.querySelector("single-page-viewer");
|
||||
e && e.style.display !== "none" && (console.log("Cleaning up single page viewer before HTMX navigation"), e.close());
|
||||
@@ -927,7 +1003,7 @@ window.addEventListener("beforeunload", function() {
|
||||
const a = document.querySelector("single-page-viewer");
|
||||
a && a.close();
|
||||
});
|
||||
class R extends HTMLElement {
|
||||
class D extends HTMLElement {
|
||||
constructor() {
|
||||
super(), this.isVisible = !1, this.scrollHandler = null, this.htmxAfterSwapHandler = null;
|
||||
}
|
||||
@@ -968,8 +1044,8 @@ class R extends HTMLElement {
|
||||
});
|
||||
}
|
||||
}
|
||||
customElements.define("scroll-to-top-button", R);
|
||||
class z extends HTMLElement {
|
||||
customElements.define("scroll-to-top-button", D);
|
||||
class j extends HTMLElement {
|
||||
constructor() {
|
||||
super(), this.pageObserver = null, this.pageContainers = /* @__PURE__ */ new Map(), this.singlePageViewerActive = !1, this.singlePageViewerCurrentPage = null, this.boundHandleSinglePageViewer = this.handleSinglePageViewer.bind(this);
|
||||
}
|
||||
@@ -1088,8 +1164,8 @@ class z extends HTMLElement {
|
||||
this.pageObserver && (this.pageObserver.disconnect(), this.pageObserver = null), document.removeEventListener("singlepageviewer:opened", this.boundHandleSinglePageViewer), document.removeEventListener("singlepageviewer:closed", this.boundHandleSinglePageViewer), document.removeEventListener("singlepageviewer:pagechanged", this.boundHandleSinglePageViewer), this.pageContainers.clear();
|
||||
}
|
||||
}
|
||||
customElements.define("inhaltsverzeichnis-scrollspy", z);
|
||||
class j extends HTMLElement {
|
||||
customElements.define("inhaltsverzeichnis-scrollspy", j);
|
||||
class F extends HTMLElement {
|
||||
constructor() {
|
||||
super(), this.innerHTML = `
|
||||
<div id="error-modal" class="fixed inset-0 bg-black bg-opacity-50 hidden z-50 flex items-center justify-center backdrop-blur-sm">
|
||||
@@ -1137,11 +1213,11 @@ class j extends HTMLElement {
|
||||
window.showErrorModal = (e) => this.show(e), window.closeErrorModal = () => this.close();
|
||||
}
|
||||
}
|
||||
customElements.define("error-modal", j);
|
||||
customElements.define("error-modal", F);
|
||||
window.currentPageContainers = window.currentPageContainers || [];
|
||||
window.currentActiveIndex = window.currentActiveIndex || 0;
|
||||
window.pageObserver = window.pageObserver || null;
|
||||
function D(a, e, t, i = null) {
|
||||
function K(a, e, t, i = null) {
|
||||
let n = document.querySelector("single-page-viewer");
|
||||
n || (n = document.createElement("single-page-viewer"), document.body.appendChild(n));
|
||||
const s = a.closest('[data-beilage="true"]') !== null, o = window.templateData && window.templateData.targetPage ? window.templateData.targetPage : 0, r = a.closest(".newspaper-page-container, .piece-page-container");
|
||||
@@ -1159,7 +1235,7 @@ function D(a, e, t, i = null) {
|
||||
function E() {
|
||||
document.getElementById("pageModal").classList.add("hidden");
|
||||
}
|
||||
function F() {
|
||||
function W() {
|
||||
if (window.pageObserver && (window.pageObserver.disconnect(), window.pageObserver = null), window.currentPageContainers = Array.from(document.querySelectorAll(".newspaper-page-container")), window.currentActiveIndex = 0, b(), document.querySelector(".newspaper-page-container")) {
|
||||
let e = /* @__PURE__ */ new Set();
|
||||
window.pageObserver = new IntersectionObserver(
|
||||
@@ -1180,7 +1256,7 @@ function F() {
|
||||
});
|
||||
}
|
||||
}
|
||||
function K() {
|
||||
function Z() {
|
||||
if (window.currentActiveIndex > 0) {
|
||||
let a = -1;
|
||||
const e = [];
|
||||
@@ -1201,7 +1277,7 @@ function K() {
|
||||
}, 100));
|
||||
}
|
||||
}
|
||||
function Z() {
|
||||
function J() {
|
||||
if (window.currentActiveIndex < window.currentPageContainers.length - 1) {
|
||||
let a = -1;
|
||||
const e = [];
|
||||
@@ -1222,7 +1298,7 @@ function Z() {
|
||||
}, 100));
|
||||
}
|
||||
}
|
||||
function W() {
|
||||
function G() {
|
||||
if (C()) {
|
||||
const e = document.querySelector("#newspaper-content .newspaper-page-container");
|
||||
e && e.scrollIntoView({
|
||||
@@ -1262,7 +1338,7 @@ function b() {
|
||||
i ? (t.title = "Zur Hauptausgabe", t.className = "w-14 h-10 lg:w-16 lg:h-12 px-2 py-1 bg-gray-100 hover:bg-gray-200 text-gray-700 hover:text-gray-800 border border-gray-300 transition-colors duration-200 flex items-center justify-center cursor-pointer", n && (n.className = "ri-file-text-line text-lg lg:text-xl")) : (t.title = "Zu Beilage", t.className = "w-14 h-10 lg:w-16 lg:h-12 px-2 py-1 bg-amber-100 hover:bg-amber-200 text-amber-700 hover:text-amber-800 border border-amber-300 transition-colors duration-200 flex items-center justify-center cursor-pointer", n && (n.className = "ri-attachment-line text-lg lg:text-xl"));
|
||||
}
|
||||
}
|
||||
function J() {
|
||||
function Y() {
|
||||
const a = document.getElementById("shareLinkBtn");
|
||||
let e = "";
|
||||
if (window.currentActiveIndex !== void 0 && window.currentPageContainers && window.currentPageContainers[window.currentActiveIndex]) {
|
||||
@@ -1297,7 +1373,7 @@ function x(a, e) {
|
||||
}
|
||||
}
|
||||
}
|
||||
function Y() {
|
||||
function U() {
|
||||
const a = document.getElementById("citationBtn"), e = document.title || "KGPZ";
|
||||
let t = window.location.origin + window.location.pathname;
|
||||
t.includes("#") && (t = t.split("#")[0]);
|
||||
@@ -1350,7 +1426,7 @@ function g(a, e) {
|
||||
}, 200);
|
||||
}, 2e3);
|
||||
}
|
||||
function G(a, e, t = !1) {
|
||||
function X(a, e, t = !1) {
|
||||
let i = "";
|
||||
if (t)
|
||||
i = window.location.origin + window.location.pathname + `#beilage-1-page-${a}`;
|
||||
@@ -1382,7 +1458,7 @@ function G(a, e, t = !1) {
|
||||
}
|
||||
}
|
||||
}
|
||||
function U(a, e) {
|
||||
function Q(a, e) {
|
||||
const t = document.title || "KGPZ", i = window.location.pathname.split("/");
|
||||
let n;
|
||||
if (i.length >= 3) {
|
||||
@@ -1411,7 +1487,7 @@ function U(a, e) {
|
||||
}
|
||||
}
|
||||
function L() {
|
||||
F(), window.addEventListener("scroll", function() {
|
||||
W(), window.addEventListener("scroll", function() {
|
||||
clearTimeout(window.scrollTimeout), window.scrollTimeout = setTimeout(() => {
|
||||
b();
|
||||
}, 50);
|
||||
@@ -1448,21 +1524,21 @@ function k() {
|
||||
"page-edition"
|
||||
), a.includes("/akteure/") || a.includes("/autoren") ? e.classList.add("page-akteure") : a.match(/\/\d{4}\/\d+/) ? e.classList.add("page-ausgabe") : a.includes("/search") || a.includes("/suche") ? e.classList.add("page-search") : a.includes("/ort/") ? e.classList.add("page-ort") : a.includes("/kategorie/") ? e.classList.add("page-kategorie") : a.includes("/beitrag/") ? e.classList.add("page-piece") : a.includes("/edition") && e.classList.add("page-edition");
|
||||
}
|
||||
window.enlargePage = D;
|
||||
window.enlargePage = K;
|
||||
window.closeModal = E;
|
||||
window.scrollToPreviousPage = K;
|
||||
window.scrollToNextPage = Z;
|
||||
window.scrollToBeilage = W;
|
||||
window.shareCurrentPage = J;
|
||||
window.generateCitation = Y;
|
||||
window.copyPagePermalink = G;
|
||||
window.generatePageCitation = U;
|
||||
window.scrollToPreviousPage = Z;
|
||||
window.scrollToNextPage = J;
|
||||
window.scrollToBeilage = G;
|
||||
window.shareCurrentPage = Y;
|
||||
window.generateCitation = U;
|
||||
window.copyPagePermalink = X;
|
||||
window.generatePageCitation = Q;
|
||||
k();
|
||||
P();
|
||||
document.querySelector(".newspaper-page-container") && L();
|
||||
let X = function(a) {
|
||||
let _ = function(a) {
|
||||
k(), P(), S(), setTimeout(() => {
|
||||
document.querySelector(".newspaper-page-container") && L();
|
||||
}, 50);
|
||||
};
|
||||
document.body.addEventListener("htmx:afterSettle", X);
|
||||
document.body.addEventListener("htmx:afterSettle", _);
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,14 +1,13 @@
|
||||
<div class="max-w-7xl mx-auto px-8 py-8">
|
||||
<div class="mb-10 2xl:mb-0">
|
||||
<div class="max-w-none mx-auto px-8">
|
||||
<div class="mb-10 2xl:mb-0 sticky top-0 ">
|
||||
{{ template "_header_with_toggle" .model }}
|
||||
</div>
|
||||
|
||||
<!-- Two Column Layout: Scrollspy + Content -->
|
||||
<div class="flex gap-8">
|
||||
{{ template "_scrollspy_layout" .model }}
|
||||
|
||||
<!-- People List - Main Content -->
|
||||
<div class="flex-1 space-y-6 bg-white">
|
||||
<div class="flex-1 space-y-6 bg-white mt-4">
|
||||
{{ range $_, $id := $.model.Sorted }}
|
||||
{{ $a := index $.model.Agents $id }}
|
||||
<div id="author-{{ $id }}" class="p-6 scroll-mt-8 author-section">
|
||||
@@ -17,4 +16,4 @@
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,23 +1,40 @@
|
||||
<div class="max-w-7xl mx-auto px-8 py-8">
|
||||
<div class="mb-10 2xl:mb-0">
|
||||
{{ template "_header_with_toggle" .model }}
|
||||
|
||||
<!-- Alphabet Navigation -->
|
||||
{{ template "_alphabet_nav" .model }}
|
||||
</div>
|
||||
|
||||
<!-- Two Column Layout: Scrollspy + Content -->
|
||||
<div class="max-w-none mx-auto px-8">
|
||||
<!-- Two Column Layout: Header+Scrollspy + Content -->
|
||||
<div class="flex gap-8">
|
||||
<!-- Left Column: Header + Scrollspy -->
|
||||
{{ template "_scrollspy_layout" .model }}
|
||||
|
||||
<!-- Mobile Header (shown on smaller screens) -->
|
||||
<div class="2xl:hidden mb-10 w-full">
|
||||
{{ template "_header_with_toggle" .model }}
|
||||
</div>
|
||||
|
||||
<!-- People List - Main Content -->
|
||||
<div class="flex-1 space-y-6 bg-white">
|
||||
<div class="flex-1 space-y-6 flex flex-col">
|
||||
<!-- Alphabet Navigation -->
|
||||
<div class="mb-6 w-full pt-4">
|
||||
<div class="bg-white px-8 py-4 rounded">
|
||||
<div class="mx-auto flex flex-row flex-wrap gap-x-6 gap-y-3 w-fit items-end leading-none justify-center">
|
||||
{{ range $_, $l := .model.AvailableLetters }}
|
||||
{{ if eq $l (Upper $.model.Search) }}
|
||||
<!-- This is the active letter -->
|
||||
<span class="no-underline leading-none !m-0 !p-0 text-3xl font-bold text-red-600 pointer-events-none" aria-current="true">{{ $l }}</span>
|
||||
{{ else }}
|
||||
<!-- This is an inactive letter -->
|
||||
<a href="/akteure/{{ $l }}" class="no-underline leading-none !m-0 !p-0 text-xl font-medium text-gray-700 hover:text-red-600 transition-colors">{{ $l }}</a>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<dv class="flex-1 space-y-6 bg-white">
|
||||
{{ range $_, $id := $.model.Sorted }}
|
||||
{{ $a := index $.model.Agents $id }}
|
||||
<div id="author-{{ $id }}" class="p-6 scroll-mt-8 author-section">
|
||||
{{ template "_akteur" $a }}
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
{{- $sorted := .Sorted -}}
|
||||
|
||||
<div class="hidden 2xl:block w-96 flex-shrink-0">
|
||||
<div class="sticky top-0 max-h-screen bg-white rounded py-4 flex flex-col">
|
||||
<div class="sticky top-0 max-h-screen flex flex-col">
|
||||
<!-- Compact header for 2xl+ screens -->
|
||||
<div class="px-3 pb-4 border-b border-gray-200 mb-4">
|
||||
<div class="px-3 pb-4 border-b border-gray-200 bg-slate-50 rounded-t py-8">
|
||||
{{ if eq .Search "autoren" }}
|
||||
<h2 class="text-2xl font-bold font-serif text-gray-900 mb-1">Autor:innen</h2>
|
||||
<p class="text-base text-gray-600 mb-3">Personen, die Beiträge in der Zeitung verfasst haben</p>
|
||||
@@ -47,7 +47,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav class="flex-1 overflow-y-auto overscroll-contain relative" id="scrollspy-nav">
|
||||
<nav class="flex-1 py-4 overflow-y-auto overscroll-contain relative bg-white rounded-b" id="scrollspy-nav">
|
||||
<!-- Sliding red background element -->
|
||||
<div id="scrollspy-slider" class="absolute bg-red-100 rounded-sm transition-all duration-300 ease-out opacity-0 z-0" style="width: calc(100% - 1.5rem); height: 0; top: 0; left: 0.75rem;"></div>
|
||||
|
||||
|
||||
@@ -126,7 +126,7 @@
|
||||
{{- $placeObj := GetPlace $placeRef.Ref -}}
|
||||
{{- if gt (len $placeObj.Names) 0 -}}
|
||||
{{- $placeName := index $placeObj.Names 0 -}}
|
||||
{{- $placeTag = printf "%s <a href=\"/ort/%s\"><span class=\"place-tag inline-block bg-slate-200 text-slate-700 text-xs px-2 py-0.5 rounded-md whitespace-nowrap\">%s</span></a>" $placeTag $placeObj.ID $placeName -}}
|
||||
{{- $placeTag = printf "%s <a href=\"/ort/%s\" class=\"ml-0\"><span class=\"place-tag inline-block bg-slate-200 text-slate-700 text-xs px-2 py-0.5 rounded-md whitespace-nowrap hover:bg-slate-300 hover:text-slate-800 transition-colors duration-150\">%s</span></a>" $placeTag $placeObj.ID $placeName -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
@@ -134,7 +134,7 @@
|
||||
{{- /* Author prefix for colon format (place view) */ -}}
|
||||
{{- $colonPrefix := "" -}}
|
||||
{{- if $useColonFormat -}}
|
||||
{{- $colonPrefix = ": " -}}
|
||||
{{- $colonPrefix = ":" -}}
|
||||
{{- else -}}
|
||||
{{- $colonPrefix = " mit " -}}
|
||||
{{- end -}}
|
||||
@@ -561,7 +561,7 @@
|
||||
{{- end -}}
|
||||
{{- if gt (len $authorElements) 0 -}}
|
||||
{{- if $title }}: <em>{{ $title }}</em>{{ end }}{{ if $workTitle }}
|
||||
{{ if $title }}, {{ end }}{{ if $categoryFlags.Uebersetzung }}Übersetzung aus:{{ else }}Auszug aus:{{ end }} {{ if $workAuthorName }}
|
||||
{{ if and $title (not $categoryFlags.Uebersetzung) }}, {{ end }}{{ if $categoryFlags.Uebersetzung }}Übersetzung aus:{{ else }}Auszug aus:{{ end }} {{ if $workAuthorName }}
|
||||
<a href="/akteure/{{ $workAuthorID }}" class="author-link text-slate-700 hover:text-slate-900 underline decoration-slate-400 hover:decoration-slate-600">{{ $workAuthorName }}</a>, {{ end }}<em
|
||||
class="work-title"
|
||||
data-short-title="{{ $workTitle }}"
|
||||
@@ -570,7 +570,7 @@
|
||||
{{ end }}
|
||||
{{- else -}}
|
||||
{{- if $title }}<em>{{ $title }}</em>{{ end }}{{ if $workTitle }}
|
||||
{{ if $title }}, {{ end }}{{ if $categoryFlags.Uebersetzung }}Übersetzung aus:{{ else }}Auszug aus:{{ end }} {{ if $workAuthorName }}
|
||||
{{ if and $title (not $categoryFlags.Uebersetzung) }}, {{ end }}{{ if $categoryFlags.Uebersetzung }}Übersetzung aus:{{ else }}Auszug aus:{{ end }} {{ if $workAuthorName }}
|
||||
<a href="/akteure/{{ $workAuthorID }}" class="author-link text-slate-700 hover:text-slate-900 underline decoration-slate-400 hover:decoration-slate-600">{{ $workAuthorName }}</a>, {{ end }}<em
|
||||
class="work-title"
|
||||
data-short-title="{{ $workTitle }}"
|
||||
@@ -580,7 +580,7 @@
|
||||
{{- end -}}{{ Safe $placeTag }}
|
||||
{{- else -}}
|
||||
{{ Safe $fortsPrefix }}{{ if $title }}<em>{{ $title }}</em>{{ end }}{{ if $workTitle }}
|
||||
{{ if $title }}, {{ end }}{{ if $categoryFlags.Uebersetzung }}Übersetzung aus:{{ else }}Auszug aus:{{ end }} {{ if $workAuthorName }}
|
||||
{{ if and $title (not $categoryFlags.Uebersetzung) }}, {{ end }}{{ if $categoryFlags.Uebersetzung }}Übersetzung aus:{{ else }}Auszug aus:{{ end }} {{ if $workAuthorName }}
|
||||
<a href="/akteure/{{ $workAuthorID }}" class="author-link text-slate-700 hover:text-slate-900 underline decoration-slate-400 hover:decoration-slate-600">{{ $workAuthorName }}</a>, {{ end }}<em
|
||||
class="work-title"
|
||||
data-short-title="{{ $workTitle }}"
|
||||
@@ -590,7 +590,7 @@
|
||||
{{- end -}}
|
||||
{{- else -}}
|
||||
{{ Safe $fortsPrefix }}{{ if $title }}<em>{{ $title }}</em>{{ end }}{{ if $workTitle }}
|
||||
{{ if $title }}, {{ end }}{{ if $categoryFlags.Uebersetzung }}Übersetzung aus:{{ else }}Auszug aus:{{ end }} {{ if $workAuthorName }}
|
||||
{{ if and $title (not $categoryFlags.Uebersetzung) }}, {{ end }}{{ if $categoryFlags.Uebersetzung }}Übersetzung aus:{{ else }}Auszug aus:{{ end }} {{ if $workAuthorName }}
|
||||
<a href="/akteure/{{ $workAuthorID }}" class="author-link text-slate-700 hover:text-slate-900 underline decoration-slate-400 hover:decoration-slate-600">{{ $workAuthorName }}</a>, {{ end }}<em
|
||||
class="work-title"
|
||||
data-short-title="{{ $workTitle }}"
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<div class="flex flex-row justify-center gap-4 h-96" id="filter">
|
||||
<div class="p-4 w-full max-w-md flex flex-col h-full">
|
||||
<div class="p-4 w-full max-w-sm flex flex-col h-full">
|
||||
<h3 class="text-lg font-semibold text-slate-800 mb-4 flex items-center gap-2">
|
||||
<i class="ri-calendar-line text-slate-600"></i>
|
||||
Auswahl nach Datum, Nummer od. Seite
|
||||
Nach Datum, Nummer oder Seite
|
||||
</h3>
|
||||
|
||||
<!-- Unified Year Jump Filter -->
|
||||
@@ -54,11 +54,11 @@
|
||||
</year-jump-filter>
|
||||
</div>
|
||||
|
||||
<div class="p-4 w-full max-w-md flex flex-col h-full">
|
||||
<div class="p-4 w-full max-w-sm flex flex-col h-full">
|
||||
<h3 class="text-lg font-semibold text-slate-800 mb-4 flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<i class="ri-user-line text-slate-600"></i>
|
||||
Auswahl nach Person
|
||||
Nach Person
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
<i class="ri-arrow-right-line"></i>
|
||||
@@ -123,11 +123,11 @@
|
||||
</person-jump-filter>
|
||||
</div>
|
||||
|
||||
<div class="p-4 w-full max-w-md flex flex-col h-full">
|
||||
<div class="p-4 w-full max-w-sm flex flex-col h-full">
|
||||
<h3 class="text-lg font-semibold text-slate-800 mb-4 flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<i class="ri-map-pin-line text-slate-600"></i>
|
||||
Auswahl nach Ort
|
||||
Nach Ort
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
<i class="ri-arrow-right-line"></i>
|
||||
@@ -169,5 +169,49 @@
|
||||
</div>
|
||||
</place-jump-filter>
|
||||
</div>
|
||||
|
||||
<div class="p-4 w-full max-w-sm flex flex-col h-full">
|
||||
<h3 class="text-lg font-semibold text-slate-800 mb-4 flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<i class="ri-price-tag-3-line text-slate-600"></i>
|
||||
Nach Kategorie
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
<i class="ri-arrow-right-line"></i>
|
||||
<a href="/kategorie/" class="text-sm text-slate-600 hover:underline">Kategorien</a>
|
||||
</div>
|
||||
</h3>
|
||||
|
||||
<!-- Category Jump Filter -->
|
||||
<category-jump-filter class="flex-1 flex flex-col min-h-0">
|
||||
<div class="space-y-3 flex flex-col h-full">
|
||||
<div class="flex items-center gap-2">
|
||||
<label for="category-search" class="hidden text-sm text-slate-600 w-16">Filter</label>
|
||||
<input
|
||||
type="text"
|
||||
id="category-search"
|
||||
placeholder="Kategorie eingeben..."
|
||||
autocomplete="off"
|
||||
class="flex-1 px-2 py-1 border border-slate-300 rounded text-sm bg-white focus:outline-none focus:ring-1 focus:ring-blue-400 focus:border-blue-400"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 min-h-0 overflow-y-auto border border-slate-300 rounded bg-white text-sm">
|
||||
<!-- All Categories List -->
|
||||
<div id="all-categories">
|
||||
{{ range $category := .Categories }}
|
||||
<div class="category-item odd:bg-slate-50 even:bg-white">
|
||||
<a href="/kategorie/{{ $category.ID }}" class="block px-2 py-1 hover:bg-blue-50 border-b border-slate-100 last:border-b-0">
|
||||
<span class="category-name font-medium text-slate-800">{{ $category.Name }}</span>
|
||||
<span class="text-xs text-slate-400 ml-2">ab {{ $category.FirstYear }}</span>
|
||||
</a>
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</category-jump-filter>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
101
views/routes/kategorie/list/body.gohtml
Normal file
101
views/routes/kategorie/list/body.gohtml
Normal file
@@ -0,0 +1,101 @@
|
||||
{{- /* Categories list page body */ -}}
|
||||
<div class="grid grid-cols-1 lg:grid-cols-4 gap-6">
|
||||
{{- /* Main content */ -}}
|
||||
<div class="lg:col-span-3">
|
||||
{{- /* Categories grid */ -}}
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4 mt-6">
|
||||
{{- range $categoryID := .model.Sorted -}}
|
||||
{{- $categoryData := index $.model.Categories $categoryID -}}
|
||||
{{- $category := $categoryData.Category -}}
|
||||
{{- $readable := $categoryData.Readable -}}
|
||||
<div class="bg-white rounded p-4 hover:bg-slate-50 transition-colors duration-200"
|
||||
data-filter-item
|
||||
data-category-name="{{ if $category.Names }}{{ index $category.Names 0 }}{{ else }}{{ $category.ID }}{{ end }}"
|
||||
data-category-description="{{ if $readable.Annotations }}{{ $readable.Annotations | html }}{{ end }}{{ if $readable.Notes }} {{ $readable.Notes | html }}{{ end }}">
|
||||
<div class="mb-3">
|
||||
<h2 class="text-lg font-medium mb-1 text-slate-900">
|
||||
{{ if $category.Names }}{{ index $category.Names 0 }}{{ else }}{{ $category.ID }}{{ end }}
|
||||
</h2>
|
||||
<p class="text-sm text-slate-500">
|
||||
{{ $categoryData.PieceCount }} {{ if eq $categoryData.PieceCount 1 }}Beitrag{{ else }}Beiträge{{ end }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{{- /* Show category annotations and notes */ -}}
|
||||
{{- if $readable.AnnotationsHTML -}}
|
||||
<div class="text-sm text-slate-700 mb-2">
|
||||
{{- range $annotation := $readable.AnnotationsHTML -}}
|
||||
{{ $annotation }}
|
||||
{{- end -}}
|
||||
</div>
|
||||
{{- end -}}
|
||||
{{- if $readable.NotesHTML -}}
|
||||
<div class="text-sm text-slate-600 italic">
|
||||
{{- range $note := $readable.NotesHTML -}}
|
||||
{{ $note }}
|
||||
{{- end -}}
|
||||
</div>
|
||||
{{- end -}}
|
||||
|
||||
{{- /* Available years */ -}}
|
||||
{{- if $categoryData.AvailableYears -}}
|
||||
<div class="mt-3 pt-2 border-t border-slate-100">
|
||||
<div class="flex flex-wrap gap-1">
|
||||
{{- range $yearData := $categoryData.AvailableYears -}}
|
||||
<a href="/kategorie/{{ $category.ID }}/{{ $yearData.Year }}"
|
||||
class="inline-block px-2 py-1 text-xs bg-blue-100 text-blue-700 rounded hover:bg-blue-200 hover:text-blue-800 transition-colors duration-150"
|
||||
title="{{ $yearData.PieceCount }} {{ if eq $yearData.PieceCount 1 }}Beitrag{{ else }}Beiträge{{ end }}">
|
||||
{{ $yearData.Year }}
|
||||
</a>
|
||||
{{- end -}}
|
||||
</div>
|
||||
</div>
|
||||
{{- end -}}
|
||||
</div>
|
||||
{{- end -}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{- /* Sidebar */ -}}
|
||||
<div class="lg:col-span-1 sticky top-0 self-start">
|
||||
<div class="bg-slate-50 p-6 filter-sidebar">
|
||||
<h1 class="text-2xl font-bold text-slate-800 mb-4">Kategorien</h1>
|
||||
|
||||
<p class="text-slate-600 mb-6">
|
||||
Verzeichnis aller Kategorien von Beiträgen in der Königsberger Gelehrten und Politischen Zeitung
|
||||
</p>
|
||||
|
||||
{{- /* Search Filter */ -}}
|
||||
<div class="mb-4">
|
||||
<generic-filter
|
||||
placeholder="Kategorien durchsuchen..."
|
||||
item-selector="[data-filter-item]"
|
||||
search-attributes="data-category-name,data-category-description"
|
||||
count-selector="[data-filter-count]"
|
||||
item-type="Kategorien"
|
||||
item-type-singular="Kategorie">
|
||||
</generic-filter>
|
||||
</div>
|
||||
|
||||
<div class="text-sm text-slate-700 mb-4" data-filter-count>
|
||||
{{ len .model.Categories }} Kategorien
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{- /* Statistics */ -}}
|
||||
<div class="bg-slate-50 border border-slate-200 rounded-lg p-4 mt-4">
|
||||
<div class="text-sm text-slate-700">
|
||||
<p class="mb-2">
|
||||
<span class="font-medium">{{ len .model.Categories }}</span> Kategorien verfügbar
|
||||
</p>
|
||||
{{- $totalPieces := 0 -}}
|
||||
{{- range $categoryData := .model.Categories -}}
|
||||
{{- $totalPieces = add $totalPieces $categoryData.PieceCount -}}
|
||||
{{- end -}}
|
||||
<p>
|
||||
<span class="font-medium">{{ $totalPieces }}</span> Beiträge insgesamt
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
3
views/routes/kategorie/list/head.gohtml
Normal file
3
views/routes/kategorie/list/head.gohtml
Normal file
@@ -0,0 +1,3 @@
|
||||
{{- /* Head template for categories list page */ -}}
|
||||
<title>Kategorien — KGPZ Digital</title>
|
||||
<meta name="description" content="Übersicht aller Kategorien von Beiträgen in der Königsberger Gelehrten und Politischen Zeitung.">
|
||||
153
views/routes/kategorie/pieces/body.gohtml
Normal file
153
views/routes/kategorie/pieces/body.gohtml
Normal file
@@ -0,0 +1,153 @@
|
||||
{{- /* Category pieces page body - matches single place view layout */ -}}
|
||||
<!-- Single Category/Year Detail View -->
|
||||
<div class="max-w-7xl mx-auto px-8 py-8">
|
||||
{{- /* Year Navigation with Back Button - Outside main content box */ -}}
|
||||
{{- if .model.AvailableYears -}}
|
||||
<div class="mb-6 w-full">
|
||||
<div class="bg-white px-8 py-6 rounded">
|
||||
<div class="flex items-center justify-between gap-8">
|
||||
{{- /* Back Navigation */ -}}
|
||||
<div class="flex-shrink-0">
|
||||
<a href="/kategorie/" class="inline-flex items-center hover:text-black text-gray-600 transition-colors text-xl no-underline font-bold">
|
||||
<i class="ri-arrow-left-line mr-1 text-xl font-bold"></i>
|
||||
Kategorien
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{{- /* Year Navigation */ -}}
|
||||
<div class="flex flex-row flex-wrap gap-x-6 gap-y-3 items-end leading-none justify-center flex-1">
|
||||
{{- range $yearData := .model.AvailableYears -}}
|
||||
{{- if eq $yearData.Year $.model.Year -}}
|
||||
<!-- This is the active year -->
|
||||
<span class="no-underline leading-none !m-0 !p-0 text-3xl font-bold text-red-600 pointer-events-none" aria-current="true">{{ $yearData.Year }}</span>
|
||||
{{- else -}}
|
||||
<!-- This is an inactive year -->
|
||||
<a href="/kategorie/{{ $.model.Category.ID }}/{{ $yearData.Year }}" class="no-underline leading-none !m-0 !p-0 text-xl font-medium text-gray-700 hover:text-red-600 transition-colors" title="{{ $yearData.PieceCount }} {{ if eq $yearData.PieceCount 1 }}Beitrag{{ else }}Beiträge{{ end }}">{{ $yearData.Year }}</a>
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{- else -}}
|
||||
{{- /* Fallback: Just back navigation if no years available */ -}}
|
||||
<div class="mb-6">
|
||||
<div class="bg-white px-8 py-6 rounded">
|
||||
<a href="/kategorie/" class="inline-flex items-center hover:text-black text-gray-600 transition-colors text-xl no-underline font-bold">
|
||||
<i class="ri-arrow-left-line mr-1 text-xl font-bold"></i>
|
||||
Kategorien
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{{- end -}}
|
||||
|
||||
<div class="bg-white px-6 py-6 rounded w-full lg:min-w-[800px] xl:min-w-[900px]">
|
||||
|
||||
{{- /* Category Header */ -}}
|
||||
<div class="mb-8">
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<div class="flex-1">
|
||||
<h1 class="text-3xl font-bold text-slate-800 mb-2">
|
||||
{{ index .model.Category.Names 0 }}
|
||||
</h1>
|
||||
|
||||
{{- /* Category description */ -}}
|
||||
{{- if .model.CategoryReadable.AnnotationsHTML -}}
|
||||
<div class="text-lg text-slate-700 mb-2">
|
||||
{{- range $annotation := .model.CategoryReadable.AnnotationsHTML -}}
|
||||
<div>{{ $annotation }}</div>
|
||||
{{- end -}}
|
||||
</div>
|
||||
{{- end -}}
|
||||
|
||||
<p class="text-slate-600">
|
||||
<span class="font-medium">{{ .model.PieceCount }}</span>
|
||||
{{ if eq .model.PieceCount 1 }}Beitrag{{ else }}Beiträge{{ end }}
|
||||
in der Kategorie „{{ index .model.Category.Names 0 }}" aus dem Jahr {{ .model.Year }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{- /* Pieces List */ -}}
|
||||
<div>
|
||||
<h2 class="text-xl font-semibold text-slate-800 mb-4">
|
||||
<i class="ri-newspaper-line mr-2"></i><u class="decoration underline-offset-3">Beiträge</u> ({{ .model.PieceCount }})
|
||||
</h2>
|
||||
|
||||
{{- if .model.Pieces -}}
|
||||
<div class="space-y-6 max-w-[85ch]">
|
||||
<div>
|
||||
<h3 class="text-lg font-bold font-serif text-slate-800 mb-3">{{ .model.Year }}</h3>
|
||||
|
||||
<div class="space-y-1">
|
||||
{{- /* Group pieces by title within the year */ -}}
|
||||
{{- $groupedPieces := dict -}}
|
||||
{{- range $_, $p := .model.Pieces -}}
|
||||
{{- $groupKey := "" -}}
|
||||
{{- if $p.Title -}}
|
||||
{{- $groupKey = index $p.Title 0 -}}
|
||||
{{- else if $p.Incipit -}}
|
||||
{{- $groupKey = index $p.Incipit 0 -}}
|
||||
{{- else -}}
|
||||
{{- $groupKey = printf "untitled-%s" $p.ID -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- $existing := index $groupedPieces $groupKey -}}
|
||||
{{- if $existing -}}
|
||||
{{- $groupedPieces = merge $groupedPieces (dict $groupKey (append $existing $p)) -}}
|
||||
{{- else -}}
|
||||
{{- $groupedPieces = merge $groupedPieces (dict $groupKey (slice $p)) -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- range $groupKey, $groupedItems := $groupedPieces -}}
|
||||
<div>
|
||||
<div class="pb-1 text-lg indent-4">
|
||||
{{- /* Use first piece for display text with colon format for places */ -}}
|
||||
{{ template "_unified_piece_entry" (dict "Piece" (index $groupedItems 0) "CurrentActorID" "" "DisplayMode" "place" "ShowPlaceTags" false "UseColonFormat" true "ShowContinuation" false) }}
|
||||
|
||||
{{- /* Show all citations from all pieces in this group inline with commas */ -}}
|
||||
{{ " " }}{{- range $groupIndex, $groupItem := $groupedItems -}}
|
||||
{{- range $issueIndex, $issue := $groupItem.IssueRefs -}}
|
||||
{{- /* Only show citations for the current year */ -}}
|
||||
{{- if eq $issue.When.Year $.model.Year -}}
|
||||
{{- if or (gt $groupIndex 0) (gt $issueIndex 0) }}, {{ end -}}
|
||||
<span class="text-blue-600 hover:text-blue-700 underline decoration-dotted hover:decoration-solid [&>a]:text-blue-600 [&>a:hover]:text-blue-700">{{- template "_citation" $issue -}}</span>
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- /* Add "Ganzer Beitrag" link if piece spans multiple issues */ -}}
|
||||
{{- $firstGroupItem := index $groupedItems 0 -}}
|
||||
{{- if gt (len $firstGroupItem.IssueRefs) 1 -}}
|
||||
{{ " " }}<div class="inline-flex items-center gap-1 px-2 py-1 bg-blue-50
|
||||
hover:bg-blue-100 text-blue-700 hover:text-blue-800 border border-blue-200
|
||||
hover:border-blue-300 rounded text-xs font-medium transition-colors duration-200">
|
||||
<i class="ri-file-copy-2-line text-xs"></i>
|
||||
<a href="{{ GetPieceURL $firstGroupItem.ID }}" class="">
|
||||
Ganzer Beitrag
|
||||
</a>
|
||||
</div>
|
||||
{{- end }}
|
||||
</div>
|
||||
</div>
|
||||
{{- end -}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{- else -}}
|
||||
<div class="bg-slate-50 rounded-lg p-8 text-center">
|
||||
<div class="text-slate-500 mb-2">
|
||||
<svg class="w-12 h-12 mx-auto mb-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-lg font-medium text-slate-900 mb-2">Keine Beiträge gefunden</h3>
|
||||
<p class="text-slate-600">
|
||||
Für die Kategorie „{{ index .model.Category.Names 0 }}" wurden im Jahr {{ .model.Year }} keine Beiträge gefunden.
|
||||
</p>
|
||||
</div>
|
||||
{{- end -}}
|
||||
</div>
|
||||
</div>
|
||||
3
views/routes/kategorie/pieces/head.gohtml
Normal file
3
views/routes/kategorie/pieces/head.gohtml
Normal file
@@ -0,0 +1,3 @@
|
||||
{{- /* Head template for category pieces page */ -}}
|
||||
<title>Kategorie „{{ index .model.Category.Names 0 }}" {{ .model.Year }} — KGPZ Digital</title>
|
||||
<meta name="description" content="Beiträge der Kategorie „{{ index .model.Category.Names 0 | html }}“ aus dem Jahr {{ .model.Year }} in der Königsberger Gelehrten und Politischen Zeitung.">
|
||||
@@ -7,7 +7,7 @@
|
||||
{{ $mainPlaceName = .ID }}
|
||||
{{ end }}
|
||||
{{ $modernName := GetModernPlaceName .Geo $mainPlaceName }}
|
||||
<div class="border border-slate-200 rounded-lg hover:bg-slate-50 transition-colors h-20" data-place-name="{{ $mainPlaceName }}" data-modern-name="{{ $modernName }}">
|
||||
<div class="bg-white rounded hover:bg-slate-50 transition-colors duration-200 h-20" data-place-name="{{ $mainPlaceName }}" data-modern-name="{{ $modernName }}">
|
||||
<a href="/ort/{{ .ID }}" class="block p-4 h-full flex flex-col justify-between">
|
||||
<div class="flex items-start justify-between gap-2">
|
||||
<div class="flex-1">
|
||||
|
||||
@@ -1,27 +1,73 @@
|
||||
<!-- Places Overview -->
|
||||
<div class="max-w-7xl mx-auto px-8 py-8">
|
||||
<div class="bg-white px-6 py-6 rounded w-full">
|
||||
<h1 class="text-3xl font-bold text-slate-800 mb-8">Orte</h1>
|
||||
|
||||
<!-- Places List -->
|
||||
{{- /* Places overview page body */ -}}
|
||||
<div class="grid grid-cols-1 lg:grid-cols-4 gap-6">
|
||||
{{- /* Main content */ -}}
|
||||
<div class="lg:col-span-3">
|
||||
{{- /* Places grid */ -}}
|
||||
{{ if .model.Places }}
|
||||
<div>
|
||||
<!-- Search Filter -->
|
||||
<places-filter></places-filter>
|
||||
|
||||
<h2 class="text-lg font-semibold text-slate-700 mb-4" data-places-count>
|
||||
Alle Orte ({{ len .model.Places }})
|
||||
</h2>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 lg:min-w-[768px] xl:min-w-[1024px]">
|
||||
{{ range $placeID := .model.Sorted }}
|
||||
{{ $place := index $.model.Places $placeID }}
|
||||
{{ template "_place_card" $place }}
|
||||
{{ end }}
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4 mt-6">
|
||||
{{ range $placeID := .model.Sorted }}
|
||||
{{ $place := index $.model.Places $placeID }}
|
||||
{{ template "_place_card" $place }}
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ else }}
|
||||
<p class="text-slate-500 italic">Keine Orte gefunden.</p>
|
||||
<div class="bg-slate-50 rounded-lg p-8 text-center mt-6">
|
||||
<div class="text-slate-500 mb-2">
|
||||
<svg class="w-12 h-12 mx-auto mb-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"/>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-lg font-medium text-slate-900 mb-2">Keine Orte gefunden</h3>
|
||||
<p class="text-slate-600">Es wurden keine Orte in der Datenbank gefunden.</p>
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
|
||||
{{- /* Sidebar */ -}}
|
||||
<div class="lg:col-span-1 sticky top-0 self-start">
|
||||
<div class="bg-slate-50 p-6 filter-sidebar">
|
||||
<h1 class="text-2xl font-bold text-slate-800 mb-4">Orte</h1>
|
||||
|
||||
<p class="text-slate-600 mb-6">
|
||||
Verzeichnis aller in der Zeitung erwähnten Orte und Lokalitäten
|
||||
</p>
|
||||
|
||||
{{- /* Search Filter */ -}}
|
||||
<div class="mb-4">
|
||||
<generic-filter
|
||||
placeholder="Ortsnamen eingeben..."
|
||||
item-selector="[data-place-name]"
|
||||
search-attributes="data-place-name,data-modern-name"
|
||||
count-selector="[data-filter-count]"
|
||||
item-type="Orte"
|
||||
item-type-singular="Ort">
|
||||
</generic-filter>
|
||||
</div>
|
||||
|
||||
<div class="text-sm text-slate-700 mb-4" data-filter-count>
|
||||
Alle Orte ({{ len .model.Places }})
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{- /* Statistics */ -}}
|
||||
<div class="bg-slate-50 border border-slate-200 rounded-lg p-4 mt-4">
|
||||
<div class="text-sm text-slate-700 space-y-2">
|
||||
<div class="flex justify-between">
|
||||
<span>Orte gesamt:</span>
|
||||
<span class="font-medium">{{ len .model.Places }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span>Beiträge mit Ort:</span>
|
||||
<span class="font-medium">{{ .model.TotalPiecesWithPlaces }}</span>
|
||||
</div>
|
||||
{{- if .model.SelectedPlace -}}
|
||||
<div class="flex justify-between">
|
||||
<span>Beiträge hier:</span>
|
||||
<span class="font-medium">{{ len .model.SelectedPlace.Pieces }}</span>
|
||||
</div>
|
||||
{{- end -}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
147
views/transform/generic-filter.js
Normal file
147
views/transform/generic-filter.js
Normal file
@@ -0,0 +1,147 @@
|
||||
/**
|
||||
* Generic Filter Web Component
|
||||
* Provides search functionality for filtering cards/items in lists
|
||||
* Can be configured for different types of content (places, categories, etc.)
|
||||
*/
|
||||
export class GenericFilter extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
this.searchInput = null;
|
||||
this.itemCards = [];
|
||||
this.countElement = null;
|
||||
this.debounceTimer = null;
|
||||
this.originalCount = 0;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
// Configuration attributes
|
||||
this.placeholderText = this.getAttribute('placeholder') || 'Suchen...';
|
||||
this.itemSelector = this.getAttribute('item-selector') || '[data-filter-item]';
|
||||
this.searchAttributes = (this.getAttribute('search-attributes') || 'data-filter-text').split(',');
|
||||
this.countSelector = this.getAttribute('count-selector') || '[data-filter-count]';
|
||||
this.itemType = this.getAttribute('item-type') || 'Einträge';
|
||||
this.itemTypeSingular = this.getAttribute('item-type-singular') || 'Eintrag';
|
||||
|
||||
this.render();
|
||||
this.setupEventListeners();
|
||||
this.initializeItems();
|
||||
}
|
||||
|
||||
|
||||
disconnectedCallback() {
|
||||
this.cleanupEventListeners();
|
||||
if (this.debounceTimer) {
|
||||
clearTimeout(this.debounceTimer);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
this.innerHTML = `
|
||||
<div class="mb-6">
|
||||
<input
|
||||
type="text"
|
||||
id="generic-search"
|
||||
placeholder="${this.placeholderText}"
|
||||
autocomplete="off"
|
||||
class="w-full px-3 py-2 border border-slate-300 rounded-md text-sm bg-white focus:outline-none focus:ring-1 focus:ring-blue-400 focus:border-blue-400"
|
||||
>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
this.searchInput = this.querySelector('#generic-search');
|
||||
if (this.searchInput) {
|
||||
this.searchInput.addEventListener('input', this.handleSearchInput.bind(this));
|
||||
}
|
||||
}
|
||||
|
||||
cleanupEventListeners() {
|
||||
if (this.searchInput) {
|
||||
this.searchInput.removeEventListener('input', this.handleSearchInput.bind(this));
|
||||
}
|
||||
}
|
||||
|
||||
initializeItems() {
|
||||
// Find all items and the count element using semantic containers
|
||||
this.itemCards = Array.from(document.querySelectorAll(this.itemSelector));
|
||||
|
||||
// Count element should be in the same semantic container as the filter
|
||||
const filterContainer = this.closest('.filter-sidebar') || this.closest('.sidebar') || document;
|
||||
this.countElement = filterContainer.querySelector(this.countSelector);
|
||||
|
||||
console.log('GenericFilter initialized:', {
|
||||
itemSelector: this.itemSelector,
|
||||
itemsFound: this.itemCards.length,
|
||||
countElement: this.countElement,
|
||||
searchAttributes: this.searchAttributes
|
||||
});
|
||||
|
||||
if (this.countElement) {
|
||||
this.originalCount = this.itemCards.length;
|
||||
}
|
||||
}
|
||||
|
||||
handleSearchInput(event) {
|
||||
// Clear previous debounce timer
|
||||
if (this.debounceTimer) {
|
||||
clearTimeout(this.debounceTimer);
|
||||
}
|
||||
|
||||
// Debounce the search to avoid excessive filtering
|
||||
this.debounceTimer = setTimeout(() => {
|
||||
this.filterItems(event.target.value.trim());
|
||||
}, 150);
|
||||
}
|
||||
|
||||
filterItems(searchTerm) {
|
||||
if (!this.itemCards.length) return;
|
||||
|
||||
const normalizedSearch = searchTerm.toLowerCase();
|
||||
let visibleCount = 0;
|
||||
|
||||
this.itemCards.forEach(card => {
|
||||
let isMatch = searchTerm === '';
|
||||
|
||||
// Check all configured search attributes
|
||||
if (!isMatch) {
|
||||
for (const attr of this.searchAttributes) {
|
||||
const attrValue = card.getAttribute(attr.trim())?.toLowerCase() || '';
|
||||
if (attrValue.includes(normalizedSearch)) {
|
||||
isMatch = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isMatch) {
|
||||
card.style.display = '';
|
||||
visibleCount++;
|
||||
} else {
|
||||
card.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
// Update the count display
|
||||
this.updateCountDisplay(visibleCount, searchTerm);
|
||||
}
|
||||
|
||||
updateCountDisplay(visibleCount, searchTerm) {
|
||||
if (!this.countElement) return;
|
||||
|
||||
if (searchTerm === '') {
|
||||
// Show original count when no search
|
||||
this.countElement.textContent = `Alle ${this.itemType} (${this.originalCount})`;
|
||||
} else if (visibleCount === 0) {
|
||||
// Show no results message
|
||||
this.countElement.textContent = `Keine ${this.itemType} gefunden für "${searchTerm}"`;
|
||||
} else {
|
||||
// Show filtered count
|
||||
const itemTypeText = visibleCount === 1 ? this.itemTypeSingular : this.itemType;
|
||||
this.countElement.textContent = `${visibleCount} von ${this.originalCount} ${itemTypeText}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Register the custom element
|
||||
customElements.define('generic-filter', GenericFilter);
|
||||
@@ -2,6 +2,7 @@ import "./site.css";
|
||||
import "./search.js";
|
||||
import "./akteure.js";
|
||||
import "./places.js";
|
||||
import "./generic-filter.js";
|
||||
import { SinglePageViewer } from "./single-page-viewer.js";
|
||||
import { ScrollToTopButton } from "./scroll-to-top.js";
|
||||
import { InhaltsverzeichnisScrollspy } from "./inhaltsverzeichnis-scrollspy.js";
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user