mirror of
https://github.com/Theodor-Springmann-Stiftung/kgpz_web.git
synced 2025-10-28 16:45:32 +00:00
Schellfilter; bugfixes; Tagewerk; Anfang Ort Controller
This commit is contained in:
135
CLAUDE.md
135
CLAUDE.md
@@ -12,7 +12,7 @@ The application follows a modular Go architecture:
|
||||
|
||||
- **Main Application**: `kgpz_web.go` - Entry point and application lifecycle management
|
||||
- **App Core**: `app/kgpz.go` - Core business logic and data processing
|
||||
- **Controllers**: Route handlers for different content types (issues, agents, places, categories, search)
|
||||
- **Controllers**: Route handlers for different content types (issues, agents, places, categories, search, quickfilters)
|
||||
- **View Models**: Data structures for template rendering with pre-processed business logic (`viewmodels/`)
|
||||
- **XML Models**: Data structures for parsing source XML files (`xmlmodels/`)
|
||||
- **Providers**: External service integrations (Git, GND, XML parsing, search)
|
||||
@@ -129,6 +129,7 @@ views/
|
||||
│ │ └── _piece_summary.gohtml # Individual piece display logic
|
||||
│ ├── datenschutz/ # Privacy policy
|
||||
│ ├── edition/ # Edition pages
|
||||
│ ├── filter/ # Quickfilter system
|
||||
│ ├── kategorie/ # Category pages
|
||||
│ ├── kontakt/ # Contact pages
|
||||
│ ├── ort/ # Places pages
|
||||
@@ -455,6 +456,138 @@ const pageUrl = `/${year}/${issue}/${pageNumber}`;
|
||||
const beilageUrl = `${window.location.pathname}#beilage-1-page-${pageNumber}`;
|
||||
```
|
||||
|
||||
## Quickfilter System (/filter)
|
||||
|
||||
The application provides a universal quickfilter system accessible from any page via a header button, offering quick access to common navigation and filtering tools.
|
||||
|
||||
### Architecture & Integration
|
||||
|
||||
**Header Integration** (`views/layouts/components/_header.gohtml` & `_menu.gohtml`):
|
||||
- **Universal Access**: Schnellfilter button available in every page header
|
||||
- **Expandable Design**: Header expands downwards to show filter content
|
||||
- **HTMX-Powered**: Dynamic loading of filter content without page refresh
|
||||
- **Seamless UI**: Integrates with existing header styling and layout
|
||||
|
||||
**Controller** (`controllers/filter_controller.go`):
|
||||
- `GetQuickFilter(kgpz *xmlmodels.Library)` - Renders filter interface
|
||||
- Uses "clear" layout for partial HTML fragments
|
||||
- Dynamically extracts available years from issue data
|
||||
|
||||
**Template System** (`views/routes/filter/body.gohtml`):
|
||||
- Clean, responsive filter interface with modern styling
|
||||
- Expandable structure for future filter options
|
||||
- Integrates existing functionality (page jump) in unified interface
|
||||
|
||||
### Current Features
|
||||
|
||||
**Page Jump Integration**:
|
||||
- **Moved from year pages**: "Direkt zu Seite springen" functionality relocated from `/jahrgang/` pages to header
|
||||
- **Universal availability**: Now accessible from any page in the application
|
||||
- **Same functionality**: Year dropdown, page input, error handling, HTMX validation
|
||||
- **Consistent UX**: Maintains all existing behavior and error feedback
|
||||
|
||||
**UI Components**:
|
||||
- **Toggle Button**: Filter icon in header with hover effects and visual feedback
|
||||
- **Expandable Container**: Header expands naturally to accommodate filter content
|
||||
- **Responsive Design**: Mobile-friendly with proper touch interactions
|
||||
- **Click-Outside Close**: Filter closes when clicking outside the container
|
||||
|
||||
### Technical Implementation
|
||||
|
||||
**URL Structure**:
|
||||
- **Filter Endpoint**: `GET /filter` - Renders filter interface using clear layout
|
||||
- **Route Configuration**: `FILTER_URL = "/filter"` defined in `app/kgpz.go`
|
||||
|
||||
**JavaScript Functionality** (`views/layouts/components/_menu.gohtml`):
|
||||
```javascript
|
||||
// Toggle filter visibility
|
||||
function toggleFilter() {
|
||||
const filterContainer = document.getElementById('filter-container');
|
||||
const filterButton = document.getElementById('filter-toggle');
|
||||
|
||||
if (filterContainer.classList.contains('hidden')) {
|
||||
filterContainer.classList.remove('hidden');
|
||||
filterButton.classList.add('bg-slate-200');
|
||||
} else {
|
||||
filterContainer.classList.add('hidden');
|
||||
filterButton.classList.remove('bg-slate-200');
|
||||
}
|
||||
}
|
||||
|
||||
// Close filter when clicking outside
|
||||
document.addEventListener('click', function(event) {
|
||||
// Automatic close functionality
|
||||
});
|
||||
```
|
||||
|
||||
**HTMX Integration**:
|
||||
```html
|
||||
<button id="filter-toggle"
|
||||
hx-get="/filter"
|
||||
hx-target="#filter-container > div"
|
||||
hx-swap="innerHTML"
|
||||
onclick="toggleFilter()">
|
||||
<i class="ri-filter-2-line"></i> Schnellfilter
|
||||
</button>
|
||||
```
|
||||
|
||||
### Layout System
|
||||
|
||||
**Header Expansion**:
|
||||
- **Natural Flow**: Filter container expands header downwards using normal document flow
|
||||
- **Content Displacement**: Page content moves down automatically when filter is open
|
||||
- **Visual Consistency**: Uses same `bg-slate-50` background as header
|
||||
- **Centered Content**: Filter content centered within expanded header area
|
||||
|
||||
**Template Structure**:
|
||||
```html
|
||||
<!-- Header container expands naturally -->
|
||||
<div id="filter-container" class="mt-6 hidden">
|
||||
<div class="flex justify-center">
|
||||
<!-- Filter content loaded here via HTMX -->
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Extensible Design
|
||||
|
||||
**Future Enhancement Ready**:
|
||||
- Modular template structure allows easy addition of new filter options
|
||||
- Controller can be extended to handle additional filter types
|
||||
- Template includes placeholder section for "Weitere Filter"
|
||||
- Architecture supports complex filtering without performance impact
|
||||
|
||||
**Data Processing**:
|
||||
- Efficient year extraction from issue data using same pattern as `year_view.go`
|
||||
- Sorted year list generation with proper deduplication
|
||||
- Ready for additional data aggregation (categories, agents, places)
|
||||
|
||||
### Usage Examples
|
||||
|
||||
**Template Integration**:
|
||||
```gohtml
|
||||
<!-- Filter automatically available in all pages via header -->
|
||||
<!-- No additional template includes needed -->
|
||||
```
|
||||
|
||||
**Controller Extension**:
|
||||
```go
|
||||
// Example of extending filter data
|
||||
data := fiber.Map{
|
||||
"AvailableYears": availableYears,
|
||||
"Categories": categories, // Future enhancement
|
||||
"TopAgents": topAgents, // Future enhancement
|
||||
}
|
||||
```
|
||||
|
||||
### Migration Impact
|
||||
|
||||
**Improved User Experience**:
|
||||
- **Reduced Page Clutter**: Removed page jump form from year overview pages
|
||||
- **Universal Access**: Page jumping now available from anywhere in the application
|
||||
- **Cleaner Year Pages**: `/jahrgang/` pages now focus purely on year navigation
|
||||
- **Consistent Interface**: Single location for all quick navigation tools
|
||||
|
||||
## Agents/Authors View System (/akteure/ and /autoren/)
|
||||
|
||||
The application provides sophisticated person and organization browsing through dual view systems with advanced navigation and filtering capabilities.
|
||||
|
||||
@@ -29,11 +29,12 @@ const (
|
||||
CONTACT_URL = "/kontakt/"
|
||||
CITATION_URL = "/zitation/"
|
||||
SEARCH_URL = "/suche/"
|
||||
FILTER_URL = "/filter"
|
||||
|
||||
INDEX_URL = "/jahrgang/1764"
|
||||
|
||||
YEAR_OVERVIEW_URL = "/jahrgang/:year"
|
||||
PLACE_OVERVIEW_URL = "/ort/:place"
|
||||
PLACE_OVERVIEW_URL = "/ort/:place?"
|
||||
AGENTS_OVERVIEW_URL = "/akteure/:letterorid"
|
||||
CATEGORY_OVERVIEW_URL = "/kategorie/:category"
|
||||
|
||||
@@ -148,6 +149,7 @@ func (k *KGPZ) Routes(srv *fiber.App) error {
|
||||
})
|
||||
|
||||
srv.Get(SEARCH_URL, controllers.GetSearch(k.Library, k.Search))
|
||||
srv.Get(FILTER_URL, controllers.GetQuickFilter(k.Library))
|
||||
srv.Get(PLACE_OVERVIEW_URL, controllers.GetPlace(k.Library))
|
||||
srv.Get(CATEGORY_OVERVIEW_URL, controllers.GetCategory(k.Library))
|
||||
srv.Get(AGENTS_OVERVIEW_URL, controllers.GetAgents(k.Library))
|
||||
|
||||
129
controllers/filter_controller.go
Normal file
129
controllers/filter_controller.go
Normal file
@@ -0,0 +1,129 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"maps"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/Theodor-Springmann-Stiftung/kgpz_web/xmlmodels"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
// GetQuickFilter handles the request to display the quick filter interface
|
||||
func GetQuickFilter(kgpz *xmlmodels.Library) fiber.Handler {
|
||||
return func(c *fiber.Ctx) error {
|
||||
// Get all available years and issues for filters
|
||||
years := make(map[int]bool)
|
||||
issuesByYear := make(map[int][]IssueSummary)
|
||||
|
||||
kgpz.Issues.Lock()
|
||||
for _, issue := range kgpz.Issues.Array {
|
||||
year := issue.Datum.When.Year
|
||||
years[year] = true
|
||||
|
||||
// Format date for display (DD.MM.YYYY)
|
||||
dateStr := fmt.Sprintf("%02d.%02d.%d",
|
||||
issue.Datum.When.Day,
|
||||
issue.Datum.When.Month,
|
||||
issue.Datum.When.Year)
|
||||
|
||||
issueSummary := IssueSummary{
|
||||
Number: issue.Number.No,
|
||||
Date: dateStr,
|
||||
}
|
||||
|
||||
issuesByYear[year] = append(issuesByYear[year], issueSummary)
|
||||
}
|
||||
kgpz.Issues.Unlock()
|
||||
|
||||
// Convert map to sorted slice using the same approach as year_view.go
|
||||
availableYears := slices.Collect(maps.Keys(years))
|
||||
slices.Sort(availableYears)
|
||||
|
||||
// Sort issues within each year by issue number
|
||||
for year := range issuesByYear {
|
||||
slices.SortFunc(issuesByYear[year], func(a, b IssueSummary) int {
|
||||
return a.Number - b.Number
|
||||
})
|
||||
}
|
||||
|
||||
// Convert issuesByYear to JSON string for the web component
|
||||
issuesByYearJSON, err := json.Marshal(issuesByYear)
|
||||
if err != nil {
|
||||
issuesByYearJSON = []byte("{}")
|
||||
}
|
||||
|
||||
// Get all persons and identify authors
|
||||
persons := make([]PersonSummary, 0)
|
||||
authors := make([]PersonSummary, 0)
|
||||
|
||||
// Find all agents who have pieces (same logic as AuthorsView)
|
||||
authorIDs := make(map[string]bool)
|
||||
for _, piece := range kgpz.Pieces.Array {
|
||||
for _, agentRef := range piece.AgentRefs {
|
||||
if agentRef.Category == "" || agentRef.Category == "autor" {
|
||||
authorIDs[agentRef.Ref] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
kgpz.Agents.Lock()
|
||||
for _, agent := range kgpz.Agents.Array {
|
||||
// Get the primary name (first name in the list)
|
||||
var name string
|
||||
if len(agent.Names) > 0 {
|
||||
name = agent.Names[0]
|
||||
} else {
|
||||
name = agent.ID // fallback to ID if no names
|
||||
}
|
||||
|
||||
person := PersonSummary{
|
||||
ID: agent.ID,
|
||||
Name: name,
|
||||
Life: agent.Life,
|
||||
}
|
||||
|
||||
persons = append(persons, person)
|
||||
|
||||
// Add to authors list if this person is an author
|
||||
if authorIDs[agent.ID] {
|
||||
authors = append(authors, person)
|
||||
}
|
||||
}
|
||||
kgpz.Agents.Unlock()
|
||||
|
||||
// Sort both lists by ID
|
||||
slices.SortFunc(persons, func(a, b PersonSummary) int {
|
||||
return strings.Compare(a.ID, b.ID)
|
||||
})
|
||||
slices.SortFunc(authors, func(a, b PersonSummary) int {
|
||||
return strings.Compare(a.ID, b.ID)
|
||||
})
|
||||
|
||||
// Prepare data for the filter template
|
||||
data := fiber.Map{
|
||||
"AvailableYears": availableYears,
|
||||
"Persons": persons,
|
||||
"Authors": authors,
|
||||
"IssuesByYearJSON": string(issuesByYearJSON),
|
||||
}
|
||||
|
||||
// Render the filter body using clear layout (no page layout)
|
||||
return c.Render("/filter/", data, "clear")
|
||||
}
|
||||
}
|
||||
|
||||
// PersonSummary represents a simplified person for the filter list
|
||||
type PersonSummary struct {
|
||||
ID string
|
||||
Name string
|
||||
Life string
|
||||
}
|
||||
|
||||
// IssueSummary represents an issue for the Jahr/Ausgabe filter
|
||||
type IssueSummary struct {
|
||||
Number int `json:"number"`
|
||||
Date string `json:"date"`
|
||||
}
|
||||
@@ -1,12 +1,40 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"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_PLACE = ""
|
||||
)
|
||||
|
||||
func GetPlace(kgpz *xmlmodels.Library) fiber.Handler {
|
||||
return func(c *fiber.Ctx) error {
|
||||
return c.Render("/ort/", nil)
|
||||
placeID := c.Params("place", DEFAULT_PLACE)
|
||||
placeID = strings.ToLower(placeID)
|
||||
|
||||
// Get places data using view model
|
||||
places := viewmodels.PlacesView(placeID, kgpz)
|
||||
|
||||
// If no places found at all, return 404
|
||||
if len(places.Places) == 0 {
|
||||
logging.Error(nil, "No places found")
|
||||
return c.SendStatus(fiber.StatusNotFound)
|
||||
}
|
||||
|
||||
// If a specific place was requested but not found, return 404
|
||||
if placeID != "" && len(placeID) > 1 && places.SelectedPlace == nil {
|
||||
logging.Error(nil, "Place not found: "+placeID)
|
||||
return c.SendStatus(fiber.StatusNotFound)
|
||||
}
|
||||
|
||||
return c.Render("/ort/", fiber.Map{
|
||||
"model": places,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,6 +151,7 @@ func Run(app *App) {
|
||||
func Engine(kgpz *app.KGPZ, c *providers.ConfigProvider) *templating.Engine {
|
||||
e := templating.NewEngine(&views.LayoutFS, &views.RoutesFS)
|
||||
e.AddFuncs(kgpz.Funcs())
|
||||
e.Globals(fiber.Map{"isDev": c.Config.Debug, "name": "KGPZ", "lang": "de"})
|
||||
timestamp := time.Now().Unix()
|
||||
e.Globals(fiber.Map{"isDev": c.Config.Debug, "name": "KGPZ", "lang": "de", "timestamp": timestamp})
|
||||
return e
|
||||
}
|
||||
|
||||
@@ -139,16 +139,11 @@ func (s *Server) Start() {
|
||||
|
||||
srv.Use(recover.New())
|
||||
|
||||
// INFO: No caching middleware in debug mode to avoid cache issues during development
|
||||
// We cant do it with cach busting the files via ?v=XXX, since we also cache the templates.
|
||||
// TODO: Dont cache static assets, bc storage gets huge on images.
|
||||
// -> Maybe fiber does this already, automatically?
|
||||
if s.Config.Debug {
|
||||
srv.Use(cache.New(cache.Config{
|
||||
Next: CacheFunc,
|
||||
Expiration: CACHE_TIME,
|
||||
CacheControl: false,
|
||||
Storage: s.cache,
|
||||
}))
|
||||
} else {
|
||||
if !s.Config.Debug {
|
||||
srv.Use(cache.New(cache.Config{
|
||||
Next: CacheFunc,
|
||||
Expiration: CACHE_TIME,
|
||||
@@ -166,7 +161,6 @@ func (s *Server) Start() {
|
||||
}
|
||||
|
||||
s.runner(srv)
|
||||
|
||||
}
|
||||
|
||||
func (s *Server) Stop() {
|
||||
@@ -232,5 +226,4 @@ func (s *Server) runner(srv *fiber.App) {
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
}
|
||||
|
||||
@@ -19,13 +19,7 @@ import (
|
||||
|
||||
const (
|
||||
ASSETS_URL_PREFIX = "/assets"
|
||||
CLEAR_LAYOUT = `
|
||||
<html>
|
||||
<head>
|
||||
{{ block "head" . }}{{ end }}
|
||||
</head>
|
||||
{{ block "body" . }}{{ end }}
|
||||
</html>`
|
||||
CLEAR_LAYOUT = `{{ block "head" . }}{{ end }}{{ block "body" . }}{{ end }}`
|
||||
)
|
||||
|
||||
type Engine struct {
|
||||
|
||||
95
viewmodels/place_view.go
Normal file
95
viewmodels/place_view.go
Normal file
@@ -0,0 +1,95 @@
|
||||
package viewmodels
|
||||
|
||||
import (
|
||||
"maps"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/Theodor-Springmann-Stiftung/kgpz_web/xmlmodels"
|
||||
)
|
||||
|
||||
// PlacesListView represents the data for the places overview
|
||||
type PlacesListView struct {
|
||||
Search string
|
||||
AvailableLetters []string
|
||||
Places map[string]xmlmodels.Place
|
||||
Sorted []string
|
||||
SelectedPlace *PlaceDetailView
|
||||
}
|
||||
|
||||
// PlaceDetailView represents a specific place with its associated pieces
|
||||
type PlaceDetailView struct {
|
||||
Place xmlmodels.Place
|
||||
Pieces []xmlmodels.Piece
|
||||
}
|
||||
|
||||
// PlacesView returns places data for the overview page
|
||||
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
|
||||
referencedPlaces := make(map[string]bool)
|
||||
for _, piece := range lib.Pieces.Array {
|
||||
for _, placeRef := range piece.PlaceRefs {
|
||||
referencedPlaces[placeRef.Ref] = true
|
||||
}
|
||||
}
|
||||
|
||||
// Build available letters and places list
|
||||
for _, place := range lib.Places.Array {
|
||||
// Only include places that are actually referenced in pieces
|
||||
if referencedPlaces[place.ID] {
|
||||
av[strings.ToUpper(place.ID[:1])] = true
|
||||
res.Sorted = append(res.Sorted, place.ID)
|
||||
res.Places[place.ID] = place
|
||||
}
|
||||
}
|
||||
|
||||
// If a specific place is requested, get its details
|
||||
if placeID != "" && len(placeID) > 1 {
|
||||
if place, exists := res.Places[placeID]; exists {
|
||||
res.SelectedPlace = GetPlaceDetail(place, lib)
|
||||
}
|
||||
}
|
||||
|
||||
res.AvailableLetters = slices.Collect(maps.Keys(av))
|
||||
slices.Sort(res.AvailableLetters)
|
||||
slices.Sort(res.Sorted)
|
||||
|
||||
return &res
|
||||
}
|
||||
|
||||
// GetPlaceDetail returns detailed information for a specific place including associated pieces
|
||||
func GetPlaceDetail(place xmlmodels.Place, lib *xmlmodels.Library) *PlaceDetailView {
|
||||
detail := &PlaceDetailView{
|
||||
Place: place,
|
||||
Pieces: make([]xmlmodels.Piece, 0),
|
||||
}
|
||||
|
||||
// Find all pieces that reference this place
|
||||
for _, piece := range lib.Pieces.Array {
|
||||
for _, placeRef := range piece.PlaceRefs {
|
||||
if placeRef.Ref == place.ID {
|
||||
detail.Pieces = append(detail.Pieces, piece)
|
||||
break // Don't add the same piece multiple times
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort pieces by title for consistent display
|
||||
slices.SortFunc(detail.Pieces, func(a, b xmlmodels.Piece) int {
|
||||
// Get first title from each piece, or use empty string if no titles
|
||||
titleA := ""
|
||||
if len(a.Title) > 0 {
|
||||
titleA = a.Title[0]
|
||||
}
|
||||
titleB := ""
|
||||
if len(b.Title) > 0 {
|
||||
titleB = b.Title[0]
|
||||
}
|
||||
return strings.Compare(strings.ToLower(titleA), strings.ToLower(titleB))
|
||||
})
|
||||
|
||||
return detail
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
28
views/layouts/components/_head.gohtml
Normal file
28
views/layouts/components/_head.gohtml
Normal file
@@ -0,0 +1,28 @@
|
||||
<meta charset="UTF-8" />
|
||||
|
||||
{{ block "head" . }}
|
||||
<!-- Default Head elements -->
|
||||
{{ end }}
|
||||
|
||||
{{ if .isDev }}
|
||||
<link rel="icon" href="/assets/logo/dev_favicon.png" />
|
||||
{{ else }}
|
||||
<link rel="icon" href="/assets/logo/favicon.png" />
|
||||
{{ end }}
|
||||
|
||||
|
||||
<link href="/assets/css/remixicon.css" rel="stylesheet" />
|
||||
<script src="/assets/scripts.js" type="module"></script>
|
||||
<script src="/assets/js/htmx.min.js" defer></script>
|
||||
<script src="/assets/js/htmx-response-targets.js" defer></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
if (typeof htmx !== 'undefined') {
|
||||
htmx.config.scrollBehavior = 'instant';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<script src="/assets/js/alpine.min.js" defer></script>
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="/assets/css/fonts.css?v={{ .timestamp }}" />
|
||||
<link rel="stylesheet" type="text/css" href="/assets/style.css?v={{ .timestamp }}" />
|
||||
@@ -5,8 +5,12 @@
|
||||
</a>
|
||||
</header>
|
||||
|
||||
<nav>
|
||||
<nav class="relative">
|
||||
{{ block "_menu" . }}
|
||||
{{ end }}
|
||||
</nav>
|
||||
|
||||
<!-- Filter Expansion Container -->
|
||||
<div id="filter-container" class="mt-6 hidden">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,14 @@
|
||||
<div class="flex flex-row justify-center mt-12 mb-8">
|
||||
<!-- Schnellfilter Button -->
|
||||
<div>
|
||||
<button
|
||||
id="filter-toggle"
|
||||
class="mr-2 text-lg border px-4 h-full hover:bg-slate-200 transition-colors cursor-pointer"
|
||||
title="Schnellfilter öffnen/schließen">
|
||||
<i class="ri-filter-2-line"></i> <div class="inline-block text-lg">Schnellauswahl</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="w-6/12">
|
||||
<input
|
||||
type="search"
|
||||
@@ -14,7 +24,7 @@
|
||||
|
||||
<div x-data="{ open: false }">
|
||||
<button
|
||||
class="ml-2 text-2xl border px-4 h-full"
|
||||
class="ml-2 text-2xl border px-4 h-full hover:bg-slate-200 transition-colors cursor-pointer"
|
||||
:class="open? 'open bg-slate-200' : 'closed'"
|
||||
@click="open = !open">
|
||||
<i class="ri-menu-line"></i>
|
||||
@@ -38,14 +48,3 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.body.addEventListener("htmx:configRequest", function (event) {
|
||||
console.log("Before request event triggered");
|
||||
let t = event.detail.elt; // Get the element triggering the request
|
||||
|
||||
if (t.id === "search" && t.value === "") {
|
||||
event.detail.parameters = {};
|
||||
event.detail.path = window.location.pathname + window.location.search;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,63 +1,31 @@
|
||||
<!doctype html>
|
||||
<html class="w-full h-full" {{ if .lang }}lang="{{ .lang }}"{{ end }}>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
|
||||
{{ block "head" . }}
|
||||
<!-- Default Head elements -->
|
||||
{{ end }}
|
||||
|
||||
{{ if .isDev }}
|
||||
<link rel="icon" href="/assets/logo/dev_favicon.png" />
|
||||
{{ else }}
|
||||
<link rel="icon" href="/assets/logo/favicon.png" />
|
||||
{{ end }}
|
||||
|
||||
|
||||
<link href="/assets/css/remixicon.css" rel="stylesheet" />
|
||||
<script>
|
||||
// Configure HTMX scroll behavior
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
if (typeof htmx !== 'undefined') {
|
||||
htmx.config.scrollBehavior = 'instant';
|
||||
}
|
||||
});
|
||||
// Fallback configuration
|
||||
window.addEventListener('load', function() {
|
||||
if (typeof htmx !== 'undefined') {
|
||||
htmx.config.scrollBehavior = 'instant';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<script src="/assets/js/alpine.min.js" defer></script>
|
||||
<script src="/assets/js/htmx.min.js" defer></script>
|
||||
<script src="/assets/js/htmx-response-targets.js" defer></script>
|
||||
<script src="/assets/js/client-side-templates.js" defer></script>
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="/assets/css/fonts.css?v=1.0" />
|
||||
<link rel="stylesheet" type="text/css" href="/assets/style.css?v=1.0" />
|
||||
|
||||
<script type="module">
|
||||
import { setup } from "/assets/scripts.js";
|
||||
setup();
|
||||
</script>
|
||||
{{ template "_head" . }}
|
||||
</head>
|
||||
|
||||
<body class="w-full" hx-ext="response-targets" hx-boost="true">
|
||||
<body class="w-full"
|
||||
hx-ext="response-targets"
|
||||
hx-boost="true"
|
||||
hx-swap="outerHTML show:window:top"
|
||||
hx-select="main"
|
||||
hx-target="main"
|
||||
hx-target-4="#error-content"
|
||||
hx-target-5="#error-content"
|
||||
>
|
||||
{{ template "_header" . }}
|
||||
<div class="container flex flex-col min-h-screen max-w-(--breakpoint-2xl) mx-auto">
|
||||
<main class="">
|
||||
{{ block "body" . }}
|
||||
<!-- Default app body... -->
|
||||
{{ end }}
|
||||
</main>
|
||||
</div>
|
||||
{{ block "_footer" . }}
|
||||
{{ end }}
|
||||
<main class="container flex flex-col min-h-screen max-w-(--breakpoint-2xl) mx-auto">
|
||||
{{ block "body" . }}
|
||||
<!-- Default app body... -->
|
||||
{{ end }}
|
||||
</main>
|
||||
{{ template "_footer" . }}
|
||||
|
||||
<!-- Scroll to Top Button -->
|
||||
<scroll-to-top-button></scroll-to-top-button>
|
||||
|
||||
{{ EmbedXSLT "xslt/transform-citation.xsl" }}
|
||||
<!-- Error Modal Web Component -->
|
||||
<error-modal></error-modal>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,35 +1,18 @@
|
||||
<!doctype html>
|
||||
<html class="w-full h-full" {{ if .lang }}lang="{{ .lang }}"{{ end }}>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
|
||||
{{ block "head" . }}
|
||||
<!-- Default Head elements -->
|
||||
{{ end }}
|
||||
|
||||
{{ if .isDev }}
|
||||
<link rel="icon" href="/assets/logo/dev_favicon.png" />
|
||||
{{ else }}
|
||||
<link rel="icon" href="/assets/logo/favicon.png" />
|
||||
{{ end }}
|
||||
|
||||
|
||||
<link href="/assets/css/remixicon.css" rel="stylesheet" />
|
||||
<script src="/assets/js/alpine.min.js" defer></script>
|
||||
<script src="/assets/js/htmx.min.js" defer></script>
|
||||
<script src="/assets/js/htmx-response-targets.js" defer></script>
|
||||
<script src="/assets/js/client-side-templates.js" defer></script>
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="/assets/css/fonts.css" />
|
||||
<link rel="stylesheet" type="text/css" href="/assets/style.css" />
|
||||
|
||||
<script type="module">
|
||||
import { setup } from "/assets/scripts.js";
|
||||
setup();
|
||||
</script>
|
||||
{{ template "_head" . }}
|
||||
</head>
|
||||
|
||||
<body class="w-full" hx-ext="response-targets" hx-boost="true">
|
||||
<body class="w-full"
|
||||
hx-ext="response-targets"
|
||||
hx-boost="true"
|
||||
hx-swap="outerHTML show:window:top"
|
||||
hx-select="main"
|
||||
hx-target="main"
|
||||
hx-target-4="#error-content"
|
||||
hx-target-5="#error-content"
|
||||
>
|
||||
<div class="flex flex-col min-h-screen">
|
||||
<!-- Header and menu with constrained width -->
|
||||
{{ template "_header" . }}
|
||||
@@ -42,15 +25,11 @@
|
||||
{{ end }}
|
||||
</main>
|
||||
|
||||
<!-- Footer with constrained width -->
|
||||
<div class="container max-w-(--breakpoint-2xl) mx-auto">
|
||||
<footer>
|
||||
{{ block "_footer" . }}
|
||||
{{ end }}
|
||||
</footer>
|
||||
</div>
|
||||
{{ template "_footer" . }}
|
||||
</div>
|
||||
|
||||
{{ EmbedXSLT "xslt/transform-citation.xsl" }}
|
||||
<!-- Error Modal Web Component -->
|
||||
<error-modal></error-modal>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
</div>
|
||||
</div>
|
||||
{{ else }}
|
||||
<div class="max-w-7xl mx-auto px-8 py-8">
|
||||
<div class="bg-white px-6 py-6 rounded">
|
||||
<div class="w-full max-w-7xl mx-auto px-8 py-8">
|
||||
<div class="bg-white px-6 py-6 rounded w-full">
|
||||
{{ template "_back_navigation" $agent }}
|
||||
{{ template "_akteur" $agent }}
|
||||
</div>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
{{ $pageCount := len $pages }}
|
||||
|
||||
<!-- Historical printing layout grid -->
|
||||
<div class="grid grid-cols-2">
|
||||
<div class="grid grid-cols-2 gap-x-2">
|
||||
{{ template "_historical_layout" (dict "pages" $pages "pageCount" $pageCount "isBeilage" false "targetPage" $.targetPage) }}
|
||||
</div>
|
||||
{{ end }}
|
||||
@@ -26,7 +26,7 @@
|
||||
{{ $pageCount := len $beilagePages }}
|
||||
|
||||
<!-- Historical printing layout grid for Beilage -->
|
||||
<div class="grid grid-cols-2">
|
||||
<div class="grid grid-cols-2 gap-x-2">
|
||||
{{ template "_historical_layout" (dict "pages" $beilagePages "pageCount" $pageCount "isBeilage" true "targetPage" $.targetPage) }}
|
||||
</div>
|
||||
</div>
|
||||
@@ -117,6 +117,9 @@
|
||||
{{ $hoverColor := "hover:border-slate-300" }}
|
||||
{{ $bgColor := "bg-blue-50" }}
|
||||
{{ $idPrefix := "page" }}
|
||||
{{ if $isBeilage }}
|
||||
{{ $idPrefix = "beilage" }}
|
||||
{{ end }}
|
||||
|
||||
{{ if $isBeilage }}
|
||||
{{ $borderColor = "border-amber-200" }}
|
||||
@@ -127,11 +130,6 @@
|
||||
|
||||
<div class="newspaper-page-container pt-4" id="{{ $idPrefix }}-{{ $page.PageNumber }}" data-page-container="{{ $page.PageNumber }}" data-page-icon-type="{{ $page.PageIcon }}"{{ if $isBeilage }} data-beilage="true"{{ end }}>
|
||||
<!-- Anchor for navigation -->
|
||||
{{ if $isBeilage }}
|
||||
<div id="beilage-{{ $page.PageNumber }}"></div>
|
||||
{{ else }}
|
||||
<div id="page-{{ $page.PageNumber }}"></div>
|
||||
{{ end }}
|
||||
<!-- Page indicator row -->
|
||||
<div class="flex {{ if $isLeft }}justify-end{{ else }}justify-start{{ end }} items-center gap-1 mb-2">
|
||||
{{ if $isLeft }}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
{{ $y := .model.Year }}
|
||||
|
||||
|
||||
<div class="mt-6 w-full">
|
||||
<div class="mx-auto flex flex-row gap-x-4 w-fit items-end leading-none">
|
||||
{{ range $year := .model.AvailableYears }}
|
||||
@@ -14,63 +13,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Page Jump Form -->
|
||||
<div class="mt-8 w-full">
|
||||
<div class="mx-auto text-center">
|
||||
<h3 class="text-lg font-medium text-slate-700 mb-6">Direkt zu Seite springen</h3>
|
||||
|
||||
<form hx-post="/jump" hx-target="#jump-errors" hx-swap="innerHTML" hx-target-4*="#jump-errors" hx-target-5*="#jump-errors" hx-ext="response-targets" class="inline-flex items-center gap-3">
|
||||
<!-- Year Selection -->
|
||||
<div class="flex items-center gap-2">
|
||||
<label for="jump-year" class="text-sm text-slate-600 whitespace-nowrap">Jahr</label>
|
||||
<select id="jump-year" name="year" value="{{ $y }}" class="px-2 py-1 border border-slate-300 rounded text-sm focus:outline-none focus:ring-1 focus:ring-blue-400 focus:border-blue-400">
|
||||
{{ range $year := .model.AvailableYears }}
|
||||
<option value="{{ $year }}" {{ if eq $year $y }}selected{{ end }}>{{ $year }}</option>
|
||||
{{ end }}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Page Input -->
|
||||
<div class="flex items-center gap-2">
|
||||
<label for="jump-page" class="text-sm text-slate-600 whitespace-nowrap">Seite</label>
|
||||
<input type="number" id="jump-page" name="page" min="1" placeholder="42" class="w-20 px-2 py-1 border border-slate-300 rounded text-sm focus:outline-none focus:ring-1 focus:ring-blue-400 focus:border-blue-400">
|
||||
</div>
|
||||
|
||||
<!-- Submit Button -->
|
||||
<button type="submit" class="inline-flex items-center px-3 py-1 bg-blue-600 hover:bg-blue-700 text-white text-sm rounded transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-1">
|
||||
<i class="ri-arrow-right-line mr-1"></i>
|
||||
Springen
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<!-- Error Display Area -->
|
||||
<div id="jump-errors" class="mt-3 min-h-[1.5rem]"></div>
|
||||
|
||||
<!-- Instructions -->
|
||||
<div class="mt-4 text-sm text-slate-500">
|
||||
<p>Geben Sie Jahr und Seitenzahl ein, um direkt zur entsprechenden Ausgabe zu springen.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Clear errors when user starts typing
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const errorContainer = document.getElementById('jump-errors');
|
||||
const yearSelect = document.getElementById('jump-year');
|
||||
const pageInput = document.getElementById('jump-page');
|
||||
|
||||
function clearErrors() {
|
||||
if (errorContainer) {
|
||||
errorContainer.innerHTML = '';
|
||||
}
|
||||
}
|
||||
|
||||
if (yearSelect) yearSelect.addEventListener('change', clearErrors);
|
||||
if (pageInput) pageInput.addEventListener('input', clearErrors);
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="grid grid-cols-11 gap-x-2 gap-y-4 pt-8">
|
||||
{{ range $index, $month := .model.Issues }}
|
||||
|
||||
|
||||
@@ -0,0 +1,238 @@
|
||||
<div class="container-normal">
|
||||
<div class="text [&_*]:!indent-0 border px-8 py-7">
|
||||
<h1 class="!mt-0">Impressum</h1>
|
||||
<div>
|
||||
<p>
|
||||
<strong>Telemedienanbieter im Sinne des § 5 TMG:</strong><br />Theodor Springmann
|
||||
Stiftung<br />Hirschgasse 2<br />69120 Heidelberg<br /><br />Telefon +49 6221 436235<br />Email <a
|
||||
href="mailto:info@theodor-springmann-stiftung.de"
|
||||
>info@theodor-springmann-stiftung.de</a
|
||||
>
|
||||
</p>
|
||||
<p>
|
||||
<strong>Rechtsform und Sitz:</strong><br />Die Theodor Springmann Stiftung ist eine
|
||||
rechtsfähige Stiftung bürgerlichen Rechts. Sitz der Stiftung ist Heidelberg.
|
||||
</p>
|
||||
<p>
|
||||
<strong>Vorstand der Theodor Springmann Stiftung:</strong><br />Dr. Randolf Straky
|
||||
(Präsident)
|
||||
</p>
|
||||
<p>
|
||||
<strong>Geschäftsführung:</strong><br />Dr. Randolf Straky<br />Theodor Springmann
|
||||
Stiftung<br />Hirschgasse 2<br />69120 Heidelberg
|
||||
</p>
|
||||
<p>
|
||||
<strong>Zuständige Aufsichtsbehörde:</strong><br />Regierungspräsidium Karlsruhe<br />Schlossplatz
|
||||
1-3<br />76131 Karlsruhe<br /><br /><a
|
||||
href="https://rp.baden-wuerttemberg.de/Themen/Stiftung/Seiten/Ansprechpartner.aspx"
|
||||
>Ansprechpartner</a
|
||||
>
|
||||
</p>
|
||||
<p>
|
||||
<strong>Redaktionelle Verantwortung nach § 55 Abs. 2 RStV:</strong><br />Janina Reibold
|
||||
<br />Theodor Springmann Stiftung<br />Hirschgasse 2<br />69120 Heidelberg
|
||||
</p>
|
||||
</div>
|
||||
2024-10-17 09:52:17.412Z
|
||||
</div>
|
||||
<div class="text">
|
||||
<h1 class="mt-12">Datenschutzerklärung</h1>
|
||||
<div class="prose">
|
||||
<div>
|
||||
<h2>Präambel</h2>
|
||||
<p>
|
||||
Diese Datenschutzerklärung informiert Sie über die Art, den Umfang und den Zweck der
|
||||
personenbezogenen Daten, die im Rahmen dieser Onlinepräsenz von Ihnen erhoben und von uns
|
||||
verarbeitet werden, sowie die Ihnen zustehenden Rechte.<br />Personenbezogene Daten sind
|
||||
alle Informationen, die sich auf eine identifizierte oder identifizierbare natürliche
|
||||
Person beziehen. Als identifizierbar wird eine natürliche Person angesehen, die direkt
|
||||
oder indirekt identifiziert werden kann. Im Hinblick auf weitere verwendete
|
||||
Begrifflichkeiten verweisen wir auf die Definitionen der Datenschutz-Grundverordnung
|
||||
(DSGVO), Artikel 4.<br />Erfolgt die Verarbeitung personenbezogener Daten auf Grundlage
|
||||
des Art. 6 Abs. 1 lit. f DSGVO, so besteht unser berechtigtes Interesse in der Erfüllung
|
||||
unseres satzungsgemäßen Stiftungszwecks. Zweck der Stiftung ist es, in gemeinnütziger
|
||||
Weise Wissenschaft und Kunst, Völkerverständigung und Entwicklungshilfe zu fördern und zum
|
||||
menschlichen Selbstverständnis sowie zum Erkennen und Lindern strukturell bedingter Not
|
||||
und Bedürftigkeit von Menschen in aller Welt beizutragen.
|
||||
</p>
|
||||
<h2>Verantwortlicher</h2>
|
||||
<p>
|
||||
Verantwortlich im Sinne von Art. 4 Abs. 7 DSGVO für die Verarbeitung personenbezogener
|
||||
Daten ist:<br />Theodor Springmann Stiftung<br />Hirschgasse 2<br />69120 Heidelberg<br /><br /><a
|
||||
href="mailto:info@theodor-springmann-stiftung.de"
|
||||
>info@theodor-springmann-stiftung.de</a
|
||||
>
|
||||
</p>
|
||||
<h2>Automatisch bei Nutzung unserer Onlinepräsenz erfasste Daten</h2>
|
||||
<p>
|
||||
Mit der Nutzung unserer Onlinepräsenz werden automatisch personenbezogene und allgemeine
|
||||
Daten vom Nutzer übermittelt und von uns erfasst und gespeichert. Die von uns erhobenen
|
||||
Daten umfassen:
|
||||
</p>
|
||||
<ol>
|
||||
<li>
|
||||
die IP-Adresse des ans Internet angeschlossenen Netzwerkgeräts (Computer oder Router des
|
||||
Nutzers),
|
||||
</li>
|
||||
<li>den verwendeten Browsertyp und dessen Version,</li>
|
||||
<li>das verwendete Betriebssystem und dessen Version,</li>
|
||||
<li>die Internetseite, die den Nutzer auf unsere Onlinepräsenz verweist (Referrer),</li>
|
||||
<li>die Unterwebseiten, die der Nutzer auf unserer Onlinepräsenz aufruft,</li>
|
||||
<li>das Datum und die Uhrzeit des Aufrufs,</li>
|
||||
<li>den Internet-Service-Provider des Nutzers,</li>
|
||||
<li>sonstige ähnliche Daten.</li>
|
||||
</ol>
|
||||
<p>
|
||||
Die Theodor Springmann Stiftung zieht aus diesen Daten keine Rückschlüsse auf die
|
||||
betroffenen Nutzer. Diese Informationen werden benötigt, um
|
||||
</p>
|
||||
<ol>
|
||||
<li>die vom Nutzer angeforderten Inhalte korrekt auszuliefern,</li>
|
||||
<li>
|
||||
technische Probleme zu diagnostizieren, unsere IT-Systeme vor Angriffen zu schützen und
|
||||
im Falle eines Angriffs den zuständigen Behörden notwendige Informationen zur
|
||||
Strafverfolgung bereitstellen zu können,
|
||||
</li>
|
||||
<li>die Inhalte und die Bedienung unserer Onlinepräsenz zu optimieren.</li>
|
||||
</ol>
|
||||
<p>
|
||||
Die oben genannten Verarbeitungszwecke werden von Subsystemen erfüllt, die unabhängig von
|
||||
anderen Subsystemen ihre jeweils eigene Kopie der vom Nutzer erhobenen Daten erhalten,
|
||||
verarbeiten und löschen. Dabei werden den Subsystemen lediglich die zur Ausführung ihrer
|
||||
Aufgabe erforderlichen Daten übergeben. Alle Subsysteme befinden sich auf Servern der
|
||||
Theodor Springmann Stiftung.
|
||||
</p>
|
||||
<h3>Auslieferung der vom Nutzer angeforderten Inhalte</h3>
|
||||
<p>
|
||||
Mit der Anforderung einer Seite dieser Onlinepräsenz übermittelt der Browser des Nutzers
|
||||
automatisch die IP-Adresse seines ans Internet angeschlossenen Netzwerkgeräts (Computer
|
||||
oder Router) und andere Daten. Ohne diese IP-Adresse können die Inhalte dieser
|
||||
Online-Präsenz nicht an den Nutzer zurückgesendet werden. Daher ist die Speicherung und
|
||||
Verarbeitung der IP-Adresse für den Betrieb dieser Onlinepräsenz notwendig. Ebenfalls
|
||||
übermittelte Daten über den verwendeten Browser usw. werden gegebenenfalls verwendet, um
|
||||
die Anzeige der Webseite an das Anzeigegerät oder den Browser anzupassen. Die zum Zwecke
|
||||
der Auslieferung von Inhalten gesammelten Daten werden direkt nach Übertragung der Inhalte
|
||||
vom Subsystem gelöscht. Die Verarbeitung dieser Daten erfolgt auf der Grundlage eines
|
||||
berechtigten Interesses nach Art. 6 Abs. 1 lit. f DSGVO.
|
||||
</p>
|
||||
<h3>Diagnose und Schutz der IT-Systeme</h3>
|
||||
<p>
|
||||
Daneben erfolgt eine Speicherung der oben genannten Daten in Diagnose-Protokollen. Die
|
||||
Protokollierung ist notwendig, um etwaige technische Probleme analysieren oder Angriffe
|
||||
erkennen und abwehren zu können. Bei Angriffen auf unsere IT-Systeme übergeben wir die
|
||||
Diagnose-Protokolle den zuständigen Strafverfolgungsbehörden. Die Diagnose-Protokolle
|
||||
werden nach 14 Tagen automatisch gelöscht. Die Speicherung und Verarbeitung dieser Daten
|
||||
erfolgt auf Grundlage eines berechtigten Interesses nach Art. 6 Abs. 1 lit. f DSGVO.
|
||||
</p>
|
||||
<h3>Analyse zur Optimierung der Inhalte und der Bedienung</h3>
|
||||
<p>
|
||||
Die oben genannten Daten werden von einem Webanalyseprogramm verarbeitet. Dabei werden die
|
||||
Daten pseudonymisiert und aggregiert, so dass sie einzelnen Nutzern nicht mehr zugeordnet
|
||||
werden können. Das Analyseprogramm wird auf einem von uns betriebenen Server ausgeführt,
|
||||
daher werden keine Daten an Dritte übermittelt. Der Zweck dieser Erfassung besteht in der
|
||||
Analyse der Nutzung unseres Angebots, durch die eine Verbesserung unserer Webseiten, der
|
||||
Inhalte und der Bedienung ermöglicht wird. Das Analyseprogramm speichert die
|
||||
anonymisierten Daten ohne zeitliche Begrenzung. Die Verarbeitung dieser Daten erfolgt auf
|
||||
der Grundlage eines berechtigten Interesses (Art. 6 Abs. 1 lit. f DSGVO).
|
||||
</p>
|
||||
<h2>Sonstige Daten</h2>
|
||||
<p>
|
||||
Sämtliche Daten, die mit dem Aufruf einer Webseite von unserer Onlinepräsenz an den Nutzer
|
||||
übertragen werden, werden von unseren Servern bereitgestellt. Wir nutzen kein von Dritten
|
||||
bereitgestelltes Content Delivery Network (CDN), um Teile unserer Onlinepräsenz (z. B.
|
||||
Javascript- oder Webfont-Dateien) von dort an den Nutzer zu übermitteln. Wir verlinken
|
||||
auch nicht in soziale Netzwerke. Insofern können von Dritten keine personenbezogenen Daten
|
||||
über unsere Onlinepräsenz erhoben werden, es sei denn, die Datenerfassung durch Dritte
|
||||
basiert auf einer gesetzlichen Grundlage und/oder wurde behördlich angeordnet.<br />In
|
||||
unseren Inhalten können sich jedoch Hyperlinks („Links“) auf fremde Onlineangebote
|
||||
befinden. Mit dem Anklicken eines solchen Links verlässt der Nutzer unsere Onlinepräsenz
|
||||
und damit den Geltungsbereich dieser Datenschutzerklärung.
|
||||
</p>
|
||||
<h2>Erhobene Daten bei Kontakt</h2>
|
||||
<p>
|
||||
Unsere Onlinepräsenz hält unter anderem aufgrund gesetzlicher Vorgaben verschiedene
|
||||
Möglichkeiten bereit, mit uns in Kontakt zu treten. Sofern eine betroffene Person Kontakt
|
||||
zu uns aufnimmt, werden die von der betroffenen Person übermittelten personenbezogenen
|
||||
Daten automatisch gespeichert (z. B. Telefonnummer, E-Mail-Adresse). Diese Angaben werden
|
||||
zum Zwecke der Bearbeitung sowie für sich möglicherweise anschließende Kommunikation von
|
||||
uns gespeichert. Sobald die Speicherung dieser Daten nicht mehr erforderlich ist und keine
|
||||
gesetzlichen Archivierungsgründe vorliegen, werden die Daten gelöscht. Die
|
||||
Erforderlichkeit wird alle zwei Jahre überprüft. Es erfolgt keine Weitergabe dieser
|
||||
personenbezogenen Daten an Dritte.<br />Von betroffenen Personen übermittelte
|
||||
Informationen zur Bearbeitung einer Kontaktanfrage werden gemäß Art. 6 Abs. 1 lit. b
|
||||
(vertragliche und vorvertragliche Beziehungen) oder lit. f (andere Anfragen) der DSGVO
|
||||
verarbeitet.
|
||||
</p>
|
||||
<h2>Speicherung und Löschung von personenbezogenen Daten</h2>
|
||||
<p>
|
||||
Personenbezogene Daten werden von uns für die Dauer der entsprechenden gesetzlichen
|
||||
Aufbewahrungspflichten oder für den zur Vertragserfüllung erforderlichen Zeitraum
|
||||
gespeichert. Liegen solche Gründe nicht vor, werden personenbezogene Daten für den
|
||||
Zeitraum, der zur Erreichung des Speicherzwecks erforderlich ist, verarbeitet und
|
||||
gespeichert, sofern in dieser Datenschutzerklärung nicht ausdrücklich anderes angegeben
|
||||
ist. Sobald die Daten für ihre Zweckbestimmung nicht mehr erforderlich sind oder der
|
||||
Speicherungszweck entfällt, werden sie nach Maßgabe der gesetzlichen Vorschriften gelöscht
|
||||
oder in ihrer Verarbeitung eingeschränkt.<br />Sollten die Daten nicht gelöscht werden
|
||||
dürfen, weil sie einer gesetzlichen Aufbewahrungspflicht unterliegen, wird deren
|
||||
Verarbeitung durch Archivierung eingeschränkt. Die Daten werden erst nach Ablauf der
|
||||
gesetzlich vorgeschriebenen Speicherfrist gelöscht.<br />Alle sechs Monate wird
|
||||
routinemäßig geprüft, ob der Speicherungszweck weggefallen bzw. die Aufbewahrungspflicht
|
||||
abgelaufen ist. Anschließend wird gegebenenfalls die Löschung durchgeführt.
|
||||
</p>
|
||||
<h2>Rechte der betroffenen Personen</h2>
|
||||
<p>
|
||||
Unter den angegebenen Kontaktdaten können betroffene Personen jederzeit nachgenannte
|
||||
Rechte ausüben. Eine betroffene Person kann von uns verlangen, dass
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
Auskunft über sie betreffende, bei uns gespeicherte Daten und deren Verarbeitung erteilt
|
||||
wird (Art. 15 DSGVO),
|
||||
</li>
|
||||
<li>
|
||||
sie betreffende unrichtige personenbezogene Daten berichtigt werden (Art. 16 DSGVO),
|
||||
</li>
|
||||
<li>
|
||||
sie betreffende, bei uns gespeicherte personenbezogene Daten gelöscht werden (Art. 17
|
||||
DSGVO),
|
||||
</li>
|
||||
<li>
|
||||
die Verarbeitung sie betreffender, bei uns gespeicherter Daten, die aufgrund
|
||||
gesetzlicher Vorschriften oder anderer Gründe nicht gelöscht werden dürfen,
|
||||
eingeschränkt wird (Art. 18 DSGVO),
|
||||
</li>
|
||||
<li>
|
||||
sie betreffende Daten übertragbar sind, soweit sie in die Datenverarbeitung eingewilligt
|
||||
oder einen Vertrag mit uns geschlossen hat (Art. 20 DSGVO),
|
||||
</li>
|
||||
<li>
|
||||
sie betreffende Daten nach einem Widerspruch nicht weiter von uns verarbeitet werden
|
||||
(Art. 21 DSGVO).
|
||||
</li>
|
||||
</ul>
|
||||
<p>Eine betroffene Person hat ferner jederzeit das Recht,</p>
|
||||
<ul>
|
||||
<li>
|
||||
eine erteilte Einwilligung in die Erhebung und Verarbeitung personenbezogener Daten nach
|
||||
Art. 6 Abs. 1 lit. a oder Art. 9 Abs. 2 lit. a DSGVO für die Zukunft zu widerrufen (Art.
|
||||
7 Abs. 3 DSGVO),
|
||||
</li>
|
||||
<li>
|
||||
sich mit einer Beschwerde an eine Aufsichtsbehörde zu wenden (Art. 15 Abs. 1 lit. f
|
||||
DSGVO). Eine Liste der Aufsichtsbehörden lässt sich unter <a
|
||||
href="https://www.bfdi.bund.de/DE/Infothek/Anschriften_Links/anschriften_links-node.html"
|
||||
>diesem Link</a
|
||||
> abrufen.
|
||||
</li>
|
||||
</ul>
|
||||
<h2>Änderung unserer Datenschutzerklärung</h2>
|
||||
<p>
|
||||
Wir behalten uns vor, diese Datenschutzerklärung ohne vorherige Ankündigung an neue
|
||||
rechtliche oder technische Sachverhalte sowie an Änderungen unserer Leistungen oder
|
||||
Prozesse anzupassen. Es gilt die jeweils auf der Onlinepräsenz veröffentlichte Version der
|
||||
Datenschutzerklärung.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
118
views/routes/filter/body.gohtml
Normal file
118
views/routes/filter/body.gohtml
Normal file
@@ -0,0 +1,118 @@
|
||||
<div class="flex flex-row justify-center gap-4" id="filter">
|
||||
<div class="bg-white border border-slate-200 rounded-lg shadow-sm p-4 w-full max-w-md">
|
||||
<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
|
||||
</h3>
|
||||
|
||||
<!-- Unified Year Jump Filter -->
|
||||
<year-jump-filter data-issues="{{ .IssuesByYearJSON }}">
|
||||
<div class="space-y-3">
|
||||
<!-- Year Selection -->
|
||||
<div class="flex items-center gap-2 mb-4">
|
||||
<label for="year-select" class="text-sm text-slate-600 w-12 hidden">Jahr wählen...</label>
|
||||
<select id="year-select" class="tabular-nums flex-1 px-2 py-1 border border-slate-300 rounded text-sm focus:outline-none focus:ring-1 focus:ring-blue-400 focus:border-blue-400" style="max-height: 200px; overflow-y: auto;">
|
||||
<option value="">Jahr wählen</option>
|
||||
{{ range $year := .AvailableYears }}
|
||||
<option value="{{ $year }}">{{ $year }}</option>
|
||||
{{ end }}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Ausgabe Selection - Two Selects -->
|
||||
<div class="flex items-center gap-2">
|
||||
<select id="issue-number-select" disabled class="flex-1 px-2 py-1 border border-slate-300 rounded text-sm focus:outline-none focus:ring-1 focus:ring-blue-400 focus:border-blue-400 disabled:bg-slate-100 disabled:cursor-not-allowed">
|
||||
<option value="">Nr.</option>
|
||||
</select>
|
||||
<select id="issue-date-select" disabled class="flex-1 px-2 py-1 border border-slate-300 rounded text-sm focus:outline-none focus:ring-1 focus:ring-blue-400 focus:border-blue-400 disabled:bg-slate-100 disabled:cursor-not-allowed">
|
||||
<option value="">Datum</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- OR Divider -->
|
||||
<div class="flex items-center my-3">
|
||||
<div class="flex-1 border-t border-slate-300"></div>
|
||||
<span class="px-2 text-xs text-slate-500 bg-white">oder</span>
|
||||
<div class="flex-1 border-t border-slate-300"></div>
|
||||
</div>
|
||||
|
||||
<!-- Page Input -->
|
||||
<div class="flex items-center gap-2">
|
||||
<label for="page-input" class="text-sm text-slate-600 w-12 hidden"> oder Seite eingeben...</label>
|
||||
<input type="number" id="page-input" min="1" placeholder="Seite eingeben" disabled class="flex-1 px-2 py-1 border border-slate-300 rounded text-sm focus:outline-none focus:ring-1 focus:ring-blue-400 focus:border-blue-400 disabled:bg-slate-100 disabled:cursor-not-allowed disabled:text-slate-500">
|
||||
</div>
|
||||
|
||||
<!-- Page Jump Button -->
|
||||
<button id="page-jump-btn" type="button" disabled class="w-full inline-flex items-center justify-center px-3 py-2 bg-slate-400 text-white text-sm rounded transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-slate-300 focus:ring-offset-1 disabled:bg-slate-400 disabled:cursor-not-allowed enabled:bg-blue-600 enabled:hover:bg-blue-700 enabled:focus:ring-blue-500">
|
||||
<i class="ri-arrow-right-line mr-2"></i>
|
||||
Zur Seite springen
|
||||
</button>
|
||||
|
||||
<!-- Error Display Area -->
|
||||
<div id="jump-errors" class="mt-2 min-h-[1rem]"></div>
|
||||
</div>
|
||||
</year-jump-filter>
|
||||
</div>
|
||||
|
||||
<div class="bg-white border border-slate-200 rounded-lg shadow-sm p-4 w-full max-w-md">
|
||||
<h3 class="text-lg font-semibold text-slate-800 mb-4 flex items-center gap-2">
|
||||
<i class="ri-user-line text-slate-600"></i>
|
||||
Auswahl nach Person
|
||||
</h3>
|
||||
|
||||
<!-- Person Jump Filter -->
|
||||
<person-jump-filter>
|
||||
<div class="space-y-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<label for="person-search" class="hidden text-sm text-slate-600 w-16">Filter</label>
|
||||
<input
|
||||
type="text"
|
||||
id="person-search"
|
||||
placeholder="Name oder Lebensdaten eingeben..."
|
||||
class="flex-1 px-2 py-1 border border-slate-300 rounded text-sm focus:outline-none focus:ring-1 focus:ring-blue-400 focus:border-blue-400"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="authors-only"
|
||||
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 focus:ring-2"
|
||||
>
|
||||
<label for="authors-only" class="text-sm text-slate-600">Nur Autor:innen anzeigen</label>
|
||||
</div>
|
||||
|
||||
<div class="max-h-48 overflow-y-auto border border-slate-300 rounded bg-white text-sm">
|
||||
<!-- All Persons List -->
|
||||
<div id="all-persons">
|
||||
{{ range $person := .Persons }}
|
||||
<div class="person-item odd:bg-slate-50 even:bg-white">
|
||||
<a href="/akteure/{{ $person.ID }}" class="block px-2 py-1 hover:bg-blue-50 border-b border-slate-100 last:border-b-0">
|
||||
<span class="person-name font-medium text-slate-800">{{ $person.Name }}</span>
|
||||
{{ if $person.Life }}
|
||||
<span class="person-life text-xs text-slate-500 ml-2">{{ $person.Life }}</span>
|
||||
{{ end }}
|
||||
</a>
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
|
||||
<!-- Authors Only List (initially hidden) -->
|
||||
<div id="authors-only-list" style="display: none;">
|
||||
{{ range $author := .Authors }}
|
||||
<div class="person-item odd:bg-slate-50 even:bg-white">
|
||||
<a href="/akteure/{{ $author.ID }}" class="block px-2 py-1 hover:bg-blue-50 border-b border-slate-100 last:border-b-0">
|
||||
<span class="person-name font-medium text-slate-800">{{ $author.Name }}</span>
|
||||
{{ if $author.Life }}
|
||||
<span class="person-life text-xs text-slate-500 ml-2">{{ $author.Life }}</span>
|
||||
{{ end }}
|
||||
</a>
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</person-jump-filter>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
{{ if .model.SelectedPlace }}
|
||||
<!-- Single Place Detail View -->
|
||||
<div class="max-w-7xl mx-auto px-8 py-8">
|
||||
<div class="bg-white px-6 py-6 rounded w-full">
|
||||
<!-- Back Navigation -->
|
||||
<div class="mb-6">
|
||||
<a href="/ort/" class="inline-flex items-center text-blue-600 hover:text-blue-700 text-sm">
|
||||
<i class="ri-arrow-left-line mr-2"></i>
|
||||
Zur<75>ck zur <20>bersicht
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Place Header -->
|
||||
<div class="mb-8">
|
||||
<h1 class="text-3xl font-bold text-slate-800 mb-2">
|
||||
{{ if .model.SelectedPlace.Place.Names }}
|
||||
{{ index .model.SelectedPlace.Place.Names 0 }}
|
||||
{{ else }}
|
||||
{{ .model.SelectedPlace.Place.ID }}
|
||||
{{ end }}
|
||||
</h1>
|
||||
{{ if .model.SelectedPlace.Place.Geo }}
|
||||
<p class="text-slate-600">
|
||||
<i class="ri-map-pin-line mr-1"></i>
|
||||
Geonames: {{ .model.SelectedPlace.Place.Geo }}
|
||||
</p>
|
||||
{{ end }}
|
||||
</div>
|
||||
|
||||
<!-- Associated Pieces -->
|
||||
<div>
|
||||
<h2 class="text-xl font-semibold text-slate-800 mb-4">
|
||||
Beitr<74>ge ({{ len .model.SelectedPlace.Pieces }})
|
||||
</h2>
|
||||
|
||||
{{ if .model.SelectedPlace.Pieces }}
|
||||
<div class="space-y-4">
|
||||
{{ range $piece := .model.SelectedPlace.Pieces }}
|
||||
<div class="border border-slate-200 rounded-lg p-4 hover:bg-slate-50">
|
||||
<h3 class="font-medium text-slate-800 mb-2">
|
||||
{{ if $piece.Title }}
|
||||
{{ index $piece.Title 0 }}
|
||||
{{ else }}
|
||||
Untitled
|
||||
{{ end }}
|
||||
</h3>
|
||||
|
||||
{{ if $piece.IssueRefs }}
|
||||
<div class="text-sm text-slate-600">
|
||||
{{ range $issueRef := $piece.IssueRefs }}
|
||||
<span class="inline-block mr-4">
|
||||
<i class="ri-calendar-line mr-1"></i>
|
||||
<a href="/{{ $issueRef.When.Year }}/{{ $issueRef.IssueNumber }}" class="text-blue-600 hover:text-blue-700">
|
||||
{{ $issueRef.When.Year }} Nr. {{ $issueRef.IssueNumber }}
|
||||
</a>
|
||||
</span>
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ else }}
|
||||
<p class="text-slate-500 italic">Keine Beitr<74>ge f<>r diesen Ort gefunden.</p>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{ else }}
|
||||
<!-- 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>
|
||||
|
||||
<!-- Available Letters Navigation -->
|
||||
{{ if .model.AvailableLetters }}
|
||||
<div class="mb-8">
|
||||
<h2 class="text-lg font-semibold text-slate-700 mb-4">Nach Anfangsbuchstabe</h2>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
{{ range $letter := .model.AvailableLetters }}
|
||||
<span class="px-3 py-2 bg-slate-100 text-slate-700 rounded text-sm">
|
||||
{{ $letter }}
|
||||
</span>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
<!-- Places List -->
|
||||
{{ if .model.Places }}
|
||||
<div>
|
||||
<h2 class="text-lg font-semibold text-slate-700 mb-4">
|
||||
Alle Orte ({{ len .model.Places }})
|
||||
</h2>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{{ range $placeID := .model.Sorted }}
|
||||
{{ $place := index $.model.Places $placeID }}
|
||||
<div class="border border-slate-200 rounded-lg hover:bg-slate-50">
|
||||
<a href="/ort/{{ $place.ID }}" class="block p-4">
|
||||
<h3 class="font-medium text-slate-800 mb-1">
|
||||
{{ if $place.Names }}
|
||||
{{ index $place.Names 0 }}
|
||||
{{ else }}
|
||||
{{ $place.ID }}
|
||||
{{ end }}
|
||||
</h3>
|
||||
{{ if $place.Geo }}
|
||||
<p class="text-sm text-slate-600">
|
||||
<i class="ri-map-pin-line mr-1"></i>
|
||||
{{ $place.Geo }}
|
||||
</p>
|
||||
{{ end }}
|
||||
</a>
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
{{ else }}
|
||||
<p class="text-slate-500 italic">Keine Orte gefunden.</p>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { ExecuteNextSettle } from "./helpers.js";
|
||||
// ===========================
|
||||
// AKTEURE/AUTHORS SCROLLSPY WEB COMPONENT
|
||||
// ===========================
|
||||
@@ -13,7 +14,7 @@ export class AkteureScrollspy extends HTMLElement {
|
||||
|
||||
connectedCallback() {
|
||||
// Small delay to ensure DOM is fully rendered after HTMX swap
|
||||
window.ExecuteNextSettle(() => {
|
||||
ExecuteNextSettle(() => {
|
||||
this.initializeScrollspyAfterDelay();
|
||||
});
|
||||
}
|
||||
@@ -68,7 +69,7 @@ export class AkteureScrollspy extends HTMLElement {
|
||||
this.manualNavigation = true;
|
||||
|
||||
target.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
behavior: "instant",
|
||||
block: "start",
|
||||
});
|
||||
|
||||
|
||||
83
views/transform/error-modal.js
Normal file
83
views/transform/error-modal.js
Normal file
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* ErrorModal Web Component
|
||||
* A reusable error modal component without shadow DOM for Tailwind compatibility
|
||||
*/
|
||||
class ErrorModal extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
// No shadow DOM - use regular DOM for Tailwind styling
|
||||
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">
|
||||
<div class="bg-white rounded-lg shadow-xl max-w-md w-full mx-4 max-h-96 overflow-y-auto">
|
||||
<div class="p-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-lg font-semibold text-red-600 flex items-center gap-2">
|
||||
<i class="ri-error-warning-line text-xl"></i>
|
||||
Fehler
|
||||
</h3>
|
||||
<button class="close-btn text-gray-400 hover:text-gray-600 transition-colors">
|
||||
<i class="ri-close-line text-xl"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="error-content text-slate-700">
|
||||
<!-- Error content will be loaded here -->
|
||||
</div>
|
||||
<div class="mt-6 flex justify-end">
|
||||
<button class="close-btn px-4 py-2 bg-slate-600 hover:bg-slate-700 text-white rounded transition-colors">
|
||||
Schließen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
this.modal = this.querySelector('#error-modal');
|
||||
this.errorContent = this.querySelector('.error-content');
|
||||
this.closeButtons = this.querySelectorAll('.close-btn');
|
||||
|
||||
this.setupEventListeners();
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
// Close button clicks
|
||||
this.closeButtons.forEach(btn => {
|
||||
btn.addEventListener('click', () => this.close());
|
||||
});
|
||||
|
||||
// Close on ESC key
|
||||
document.addEventListener('keydown', (event) => {
|
||||
if (event.key === 'Escape' && !this.modal.classList.contains('hidden')) {
|
||||
this.close();
|
||||
}
|
||||
});
|
||||
|
||||
// Close when clicking outside modal
|
||||
this.modal.addEventListener('click', (event) => {
|
||||
if (event.target === this.modal) {
|
||||
this.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
show(content) {
|
||||
this.errorContent.innerHTML = content;
|
||||
this.modal.classList.remove('hidden');
|
||||
}
|
||||
|
||||
close() {
|
||||
this.modal.classList.add('hidden');
|
||||
}
|
||||
|
||||
// Global helper functions for backward compatibility
|
||||
connectedCallback() {
|
||||
// Make functions globally available
|
||||
window.showErrorModal = (content) => this.show(content);
|
||||
window.closeErrorModal = () => this.close();
|
||||
}
|
||||
}
|
||||
|
||||
// Define the custom element
|
||||
customElements.define('error-modal', ErrorModal);
|
||||
|
||||
export { ErrorModal };
|
||||
35
views/transform/helpers.js
Normal file
35
views/transform/helpers.js
Normal file
@@ -0,0 +1,35 @@
|
||||
// This is a queue that stores functions to be executed after the DOM is settled.
|
||||
// It is used to ensure the DOM is fully rendered before executing certain actions.
|
||||
// Works as well as a DOMContentLoaded event listener.
|
||||
|
||||
const settleQueue = [];
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
ExecuteSettleQueue();
|
||||
});
|
||||
|
||||
const ExecuteNextSettle = function (fn) {
|
||||
if (typeof fn === "function") {
|
||||
settleQueue.push(fn);
|
||||
}
|
||||
};
|
||||
|
||||
const DeleteNextSettle = function (fn) {
|
||||
const index = settleQueue.indexOf(fn);
|
||||
if (index !== -1) {
|
||||
settleQueue.splice(index, 1);
|
||||
}
|
||||
};
|
||||
|
||||
const ExecuteSettleQueue = function () {
|
||||
while (settleQueue.length > 0) {
|
||||
const fn = settleQueue.shift();
|
||||
try {
|
||||
fn();
|
||||
} catch (error) {
|
||||
console.error("Error executing settle queue function:", error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export { ExecuteNextSettle, ExecuteSettleQueue, DeleteNextSettle };
|
||||
@@ -1,8 +1,11 @@
|
||||
import "./site.css";
|
||||
import "./search.js";
|
||||
import "./akteure.js";
|
||||
import { SinglePageViewer } from "./single-page-viewer.js";
|
||||
import { ScrollToTopButton } from "./scroll-to-top.js";
|
||||
import { InhaltsverzeichnisScrollspy } from "./inhaltsverzeichnis-scrollspy.js";
|
||||
import { ErrorModal } from "./error-modal.js";
|
||||
import { ExecuteSettleQueue } from "./helpers.js";
|
||||
import {
|
||||
enlargePage,
|
||||
closeModal,
|
||||
@@ -91,29 +94,7 @@ function applyPageBackdrop() {
|
||||
}
|
||||
}
|
||||
|
||||
// Function queue system for HTMX settle events
|
||||
let settleQueue = [];
|
||||
|
||||
// Global function to register functions for next settle event
|
||||
window.ExecuteNextSettle = function(fn) {
|
||||
if (typeof fn === 'function') {
|
||||
settleQueue.push(fn);
|
||||
}
|
||||
};
|
||||
|
||||
// Execute and clear the queue
|
||||
function executeSettleQueue() {
|
||||
while (settleQueue.length > 0) {
|
||||
const fn = settleQueue.shift();
|
||||
try {
|
||||
fn();
|
||||
} catch (error) {
|
||||
console.error('Error executing settle queue function:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Export functions for global access
|
||||
// Export functions for global access - moved outside setup() so they're available immediately
|
||||
window.enlargePage = enlargePage;
|
||||
window.closeModal = closeModal;
|
||||
window.scrollToPreviousPage = scrollToPreviousPage;
|
||||
@@ -124,44 +105,35 @@ window.generateCitation = generateCitation;
|
||||
window.copyPagePermalink = copyPagePermalink;
|
||||
window.generatePageCitation = generatePageCitation;
|
||||
|
||||
// INFO: This is intended to be called once on website load
|
||||
function setup() {
|
||||
// Apply page-specific backdrop styling
|
||||
applyPageBackdrop();
|
||||
// Apply page-specific backdrop styling
|
||||
applyPageBackdrop();
|
||||
|
||||
// Update citation links on initial load
|
||||
updateCitationLinks();
|
||||
// Update citation links on initial load
|
||||
updateCitationLinks();
|
||||
|
||||
// Initialize newspaper layout if present
|
||||
if (document.querySelector(".newspaper-page-container")) {
|
||||
initializeNewspaperLayout();
|
||||
}
|
||||
|
||||
// Akteure scrollspy web component will auto-initialize when present in template
|
||||
|
||||
// HTMX event handling for newspaper layout, scrollspy, and scroll-to-top button
|
||||
let htmxAfterSwapHandler = function (event) {
|
||||
// Apply page-specific backdrop styling after navigation
|
||||
applyPageBackdrop();
|
||||
// Update citation links after navigation
|
||||
updateCitationLinks();
|
||||
|
||||
// Execute all queued functions
|
||||
executeSettleQueue();
|
||||
|
||||
// Use shorter delay since afterSettle ensures DOM is ready
|
||||
setTimeout(() => {
|
||||
if (document.querySelector(".newspaper-page-container")) {
|
||||
initializeNewspaperLayout();
|
||||
}
|
||||
}, 50);
|
||||
};
|
||||
|
||||
let htmxBeforeRequestHandler = function (event) {};
|
||||
|
||||
document.body.addEventListener("htmx:afterSettle", htmxAfterSwapHandler);
|
||||
document.body.addEventListener("htmx:afterSettle", updateCitationLinks);
|
||||
document.body.addEventListener("htmx:beforeRequest", htmxBeforeRequestHandler);
|
||||
// Initialize newspaper layout if present
|
||||
if (document.querySelector(".newspaper-page-container")) {
|
||||
initializeNewspaperLayout();
|
||||
}
|
||||
|
||||
export { setup };
|
||||
// Akteure scrollspy web component will auto-initialize when present in template
|
||||
|
||||
// HTMX event handling for newspaper layout, scrollspy, and scroll-to-top button
|
||||
let htmxAfterSwapHandler = function (event) {
|
||||
// Apply page-specific backdrop styling after navigation
|
||||
applyPageBackdrop();
|
||||
// Update citation links after navigation
|
||||
updateCitationLinks();
|
||||
|
||||
// Execute all queued functions
|
||||
ExecuteSettleQueue();
|
||||
|
||||
// Use shorter delay since afterSettle ensures DOM is ready
|
||||
setTimeout(() => {
|
||||
if (document.querySelector(".newspaper-page-container")) {
|
||||
initializeNewspaperLayout();
|
||||
}
|
||||
}, 50);
|
||||
};
|
||||
|
||||
document.body.addEventListener("htmx:afterSettle", htmxAfterSwapHandler);
|
||||
|
||||
410
views/transform/search.js
Normal file
410
views/transform/search.js
Normal file
@@ -0,0 +1,410 @@
|
||||
const filterBtn = document.getElementById("filter-toggle");
|
||||
if (filterBtn) {
|
||||
filterBtn.addEventListener("click", toggleFilter);
|
||||
}
|
||||
|
||||
// Filter toggle functionality
|
||||
function toggleFilter() {
|
||||
const filterContainer = document.getElementById("filter-container");
|
||||
const filterButton = document.getElementById("filter-toggle");
|
||||
const filterContentDiv = filterContainer?.querySelector("div.flex.justify-center");
|
||||
|
||||
if (filterContainer.classList.contains("hidden")) {
|
||||
// Show the filter
|
||||
filterContainer.classList.remove("hidden");
|
||||
filterButton.classList.add("bg-slate-200");
|
||||
|
||||
// Load content only if it doesn't exist - check for actual content
|
||||
const hasContent = filterContentDiv && filterContentDiv.querySelector("div, form, h3");
|
||||
if (!hasContent) {
|
||||
htmx
|
||||
.ajax("GET", "/filter", {
|
||||
target: "#filter-container",
|
||||
select: "#filter",
|
||||
swap: "innerHTML",
|
||||
})
|
||||
.then(() => {
|
||||
console.log("HTMX request completed");
|
||||
// Re-query the element to see if it changed
|
||||
const updatedDiv = document.querySelector("#filter-container .flex.justify-center");
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log("HTMX request failed:", error);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
filterContainer.classList.add("hidden");
|
||||
filterButton.classList.remove("bg-slate-200");
|
||||
}
|
||||
}
|
||||
|
||||
// Export for global access
|
||||
window.toggleFilter = toggleFilter;
|
||||
|
||||
// Close filter when clicking outside
|
||||
document.addEventListener("click", function (event) {
|
||||
const filterContainer = document.getElementById("filter-container");
|
||||
const filterButton = document.getElementById("filter-toggle");
|
||||
|
||||
if (
|
||||
filterContainer &&
|
||||
filterButton &&
|
||||
!filterContainer.contains(event.target) &&
|
||||
!filterButton.contains(event.target)
|
||||
) {
|
||||
if (!filterContainer.classList.contains("hidden")) {
|
||||
filterContainer.classList.add("hidden");
|
||||
filterButton.classList.remove("bg-slate-200");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Handle search input logic
|
||||
document.body.addEventListener("htmx:configRequest", function (event) {
|
||||
let element = event.detail.elt;
|
||||
|
||||
if (element.id === "search" && element.value === "") {
|
||||
event.detail.parameters = {};
|
||||
event.detail.path = window.location.pathname + window.location.search;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* PersonJumpFilter - Web component for filtering persons list
|
||||
* Works with server-rendered person list and provides client-side filtering
|
||||
*/
|
||||
class PersonJumpFilter extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.setupEventListeners();
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
const searchInput = this.querySelector('#person-search');
|
||||
const authorsCheckbox = this.querySelector('#authors-only');
|
||||
const allPersonsList = this.querySelector('#all-persons');
|
||||
const authorsOnlyList = this.querySelector('#authors-only-list');
|
||||
|
||||
if (!searchInput || !authorsCheckbox || !allPersonsList || !authorsOnlyList) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Search functionality
|
||||
searchInput.addEventListener('input', (e) => {
|
||||
const query = e.target.value.toLowerCase().trim();
|
||||
this.filterPersons(query);
|
||||
});
|
||||
|
||||
// Checkbox functionality
|
||||
authorsCheckbox.addEventListener('change', () => {
|
||||
this.togglePersonsList();
|
||||
// Clear and re-apply search filter
|
||||
const query = searchInput.value.toLowerCase().trim();
|
||||
this.filterPersons(query);
|
||||
});
|
||||
}
|
||||
|
||||
togglePersonsList() {
|
||||
const authorsCheckbox = this.querySelector('#authors-only');
|
||||
const allPersonsList = this.querySelector('#all-persons');
|
||||
const authorsOnlyList = this.querySelector('#authors-only-list');
|
||||
|
||||
if (!authorsCheckbox || !allPersonsList || !authorsOnlyList) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (authorsCheckbox.checked) {
|
||||
allPersonsList.style.display = 'none';
|
||||
authorsOnlyList.style.display = 'block';
|
||||
} else {
|
||||
allPersonsList.style.display = 'block';
|
||||
authorsOnlyList.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
filterPersons(query) {
|
||||
// Filter items in the currently visible list
|
||||
const authorsCheckbox = this.querySelector('#authors-only');
|
||||
const currentList = authorsCheckbox?.checked ?
|
||||
this.querySelector('#authors-only-list') :
|
||||
this.querySelector('#all-persons');
|
||||
|
||||
if (!currentList) {
|
||||
return;
|
||||
}
|
||||
|
||||
const personItems = currentList.querySelectorAll('.person-item');
|
||||
|
||||
personItems.forEach(item => {
|
||||
const name = item.querySelector('.person-name')?.textContent || '';
|
||||
const life = item.querySelector('.person-life')?.textContent || '';
|
||||
|
||||
const matches = !query ||
|
||||
name.toLowerCase().includes(query) ||
|
||||
life.toLowerCase().includes(query);
|
||||
|
||||
if (matches) {
|
||||
item.style.display = 'block';
|
||||
} else {
|
||||
item.style.display = 'none';
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Register the custom element
|
||||
customElements.define('person-jump-filter', PersonJumpFilter);
|
||||
|
||||
/**
|
||||
* YearJumpFilter - Unified web component for Jahr-based navigation
|
||||
* Allows jumping by Jahr/Ausgabe or Jahr/Seite
|
||||
*/
|
||||
class YearJumpFilter extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
this.issuesByYear = {};
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.parseIssuesData();
|
||||
this.setupEventListeners();
|
||||
}
|
||||
|
||||
parseIssuesData() {
|
||||
// Parse issues data from data attributes
|
||||
const issuesData = this.dataset.issues;
|
||||
if (issuesData) {
|
||||
try {
|
||||
this.issuesByYear = JSON.parse(issuesData);
|
||||
} catch (e) {
|
||||
console.error('Failed to parse issues data:', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
const yearSelect = this.querySelector('#year-select');
|
||||
const issueNumberSelect = this.querySelector('#issue-number-select');
|
||||
const issueDateSelect = this.querySelector('#issue-date-select');
|
||||
const pageInput = this.querySelector('#page-input');
|
||||
const pageJumpBtn = this.querySelector('#page-jump-btn');
|
||||
|
||||
if (!yearSelect) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Year selection change handler
|
||||
yearSelect.addEventListener('change', () => {
|
||||
this.updateIssueOptions();
|
||||
this.updatePageInputState();
|
||||
this.clearPageErrors();
|
||||
});
|
||||
|
||||
// Issue number selection change handler - jump immediately
|
||||
if (issueNumberSelect) {
|
||||
issueNumberSelect.addEventListener('change', () => {
|
||||
const year = yearSelect.value;
|
||||
const issueNum = issueNumberSelect.value;
|
||||
if (year && issueNum) {
|
||||
window.location.href = `/${year}/${issueNum}`;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Issue date selection change handler - jump immediately
|
||||
if (issueDateSelect) {
|
||||
issueDateSelect.addEventListener('change', () => {
|
||||
const year = yearSelect.value;
|
||||
const issueNum = issueDateSelect.value; // value contains issue number
|
||||
if (year && issueNum) {
|
||||
window.location.href = `/${year}/${issueNum}`;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Page input handlers
|
||||
if (pageInput) {
|
||||
pageInput.addEventListener('input', () => {
|
||||
this.updatePageJumpButton();
|
||||
this.clearPageErrors();
|
||||
});
|
||||
|
||||
// Handle Enter key in page input
|
||||
pageInput.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
this.handlePageJump();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Page jump button
|
||||
if (pageJumpBtn) {
|
||||
pageJumpBtn.addEventListener('click', () => {
|
||||
this.handlePageJump();
|
||||
});
|
||||
}
|
||||
|
||||
// Page jump form submission
|
||||
const pageForm = this.querySelector('#page-jump-form');
|
||||
if (pageForm) {
|
||||
pageForm.addEventListener('submit', (e) => {
|
||||
e.preventDefault();
|
||||
this.handlePageJump();
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize everything
|
||||
this.updateIssueOptions();
|
||||
this.updatePageInputState();
|
||||
this.updatePageJumpButton();
|
||||
}
|
||||
|
||||
updateIssueOptions() {
|
||||
const yearSelect = this.querySelector('#year-select');
|
||||
const issueNumberSelect = this.querySelector('#issue-number-select');
|
||||
const issueDateSelect = this.querySelector('#issue-date-select');
|
||||
|
||||
if (!yearSelect || !issueNumberSelect || !issueDateSelect) {
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedYear = yearSelect.value;
|
||||
const issues = this.issuesByYear[selectedYear] || [];
|
||||
|
||||
// Clear existing options
|
||||
issueNumberSelect.innerHTML = '<option value="">Nr.</option>';
|
||||
issueDateSelect.innerHTML = '<option value="">Datum</option>';
|
||||
|
||||
// Add options for selected year
|
||||
issues.forEach(issue => {
|
||||
// Issue number select - just the number
|
||||
const numberOption = document.createElement('option');
|
||||
numberOption.value = issue.number;
|
||||
numberOption.textContent = issue.number;
|
||||
issueNumberSelect.appendChild(numberOption);
|
||||
|
||||
// Issue date select - date with issue number as value
|
||||
const dateOption = document.createElement('option');
|
||||
dateOption.value = issue.number; // value is still issue number for navigation
|
||||
dateOption.textContent = `${issue.date} [${issue.number}]`;
|
||||
issueDateSelect.appendChild(dateOption);
|
||||
});
|
||||
|
||||
const hasIssues = issues.length > 0 && selectedYear;
|
||||
issueNumberSelect.disabled = !hasIssues;
|
||||
issueDateSelect.disabled = !hasIssues;
|
||||
}
|
||||
|
||||
async handlePageJump() {
|
||||
const yearSelect = this.querySelector('#year-select');
|
||||
const pageInput = this.querySelector('#page-input');
|
||||
const errorContainer = this.querySelector('#jump-errors');
|
||||
|
||||
if (!yearSelect || !pageInput) {
|
||||
return;
|
||||
}
|
||||
|
||||
const year = yearSelect.value;
|
||||
const page = pageInput.value;
|
||||
|
||||
if (!year || !page) {
|
||||
this.showError('Bitte Jahr und Seite auswählen.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('year', year);
|
||||
formData.append('page', page);
|
||||
|
||||
const response = await fetch('/jump', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
redirect: 'manual'
|
||||
});
|
||||
|
||||
// Check for HTMX redirect header
|
||||
const hxRedirect = response.headers.get('HX-Redirect');
|
||||
if (hxRedirect) {
|
||||
window.location.href = hxRedirect;
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.status === 302 || response.status === 301) {
|
||||
const location = response.headers.get('Location');
|
||||
if (location) {
|
||||
window.location.href = location;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (response.ok) {
|
||||
if (errorContainer) {
|
||||
errorContainer.innerHTML = '';
|
||||
}
|
||||
} else {
|
||||
const errorText = await response.text();
|
||||
if (errorContainer) {
|
||||
errorContainer.innerHTML = errorText;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Page jump failed:', error);
|
||||
this.showError('Fehler beim Suchen der Seite.');
|
||||
}
|
||||
}
|
||||
|
||||
showError(message) {
|
||||
const errorContainer = this.querySelector('#jump-errors');
|
||||
if (errorContainer) {
|
||||
errorContainer.innerHTML = `<div class="text-red-600 text-sm mt-1">${message}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
clearPageErrors() {
|
||||
const errorContainer = this.querySelector('#jump-errors');
|
||||
if (errorContainer) {
|
||||
errorContainer.innerHTML = '';
|
||||
}
|
||||
}
|
||||
|
||||
updatePageInputState() {
|
||||
const yearSelect = this.querySelector('#year-select');
|
||||
const pageInput = this.querySelector('#page-input');
|
||||
|
||||
if (!yearSelect || !pageInput) {
|
||||
return;
|
||||
}
|
||||
|
||||
const hasYear = yearSelect.value;
|
||||
pageInput.disabled = !hasYear;
|
||||
|
||||
if (!hasYear) {
|
||||
pageInput.value = '';
|
||||
this.updatePageJumpButton();
|
||||
}
|
||||
}
|
||||
|
||||
updatePageJumpButton() {
|
||||
const yearSelect = this.querySelector('#year-select');
|
||||
const pageInput = this.querySelector('#page-input');
|
||||
const pageJumpBtn = this.querySelector('#page-jump-btn');
|
||||
|
||||
if (!yearSelect || !pageInput || !pageJumpBtn) {
|
||||
return;
|
||||
}
|
||||
|
||||
const hasYear = yearSelect.value;
|
||||
const hasPage = pageInput.value && pageInput.value.trim();
|
||||
const shouldEnable = hasYear && hasPage;
|
||||
|
||||
pageJumpBtn.disabled = !shouldEnable;
|
||||
}
|
||||
}
|
||||
|
||||
// Register the custom element
|
||||
customElements.define('year-jump-filter', YearJumpFilter);
|
||||
@@ -255,9 +255,11 @@ export class SinglePageViewer extends HTMLElement {
|
||||
document.body.style.overflow = "hidden";
|
||||
|
||||
// Dispatch event for scrollspy
|
||||
document.dispatchEvent(new CustomEvent('singlepageviewer:opened', {
|
||||
detail: { pageNumber: this.currentPageNumber, isBeilage: this.currentIsBeilage }
|
||||
}));
|
||||
document.dispatchEvent(
|
||||
new CustomEvent("singlepageviewer:opened", {
|
||||
detail: { pageNumber: this.currentPageNumber, isBeilage: this.currentIsBeilage },
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
close() {
|
||||
@@ -268,9 +270,11 @@ export class SinglePageViewer extends HTMLElement {
|
||||
document.body.style.overflow = "";
|
||||
|
||||
// Dispatch event for scrollspy
|
||||
document.dispatchEvent(new CustomEvent('singlepageviewer:closed', {
|
||||
detail: { pageNumber: this.currentPageNumber, isBeilage: this.currentIsBeilage }
|
||||
}));
|
||||
document.dispatchEvent(
|
||||
new CustomEvent("singlepageviewer:closed", {
|
||||
detail: { pageNumber: this.currentPageNumber, isBeilage: this.currentIsBeilage },
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
@@ -282,16 +286,14 @@ export class SinglePageViewer extends HTMLElement {
|
||||
|
||||
// Clean up keyboard event listeners
|
||||
if (this.keyboardHandler) {
|
||||
document.removeEventListener('keydown', this.keyboardHandler);
|
||||
document.removeEventListener("keydown", this.keyboardHandler);
|
||||
this.keyboardHandler = null;
|
||||
}
|
||||
|
||||
|
||||
// Restore background scrolling
|
||||
document.body.style.overflow = "";
|
||||
}
|
||||
|
||||
|
||||
// Generate icon HTML from Go icon type - matches templating/engine.go PageIcon function
|
||||
generateIconFromType(iconType) {
|
||||
switch (iconType) {
|
||||
@@ -314,24 +316,24 @@ export class SinglePageViewer extends HTMLElement {
|
||||
setupKeyboardNavigation() {
|
||||
// Remove any existing listener to avoid duplicates
|
||||
if (this.keyboardHandler) {
|
||||
document.removeEventListener('keydown', this.keyboardHandler);
|
||||
document.removeEventListener("keydown", this.keyboardHandler);
|
||||
}
|
||||
|
||||
// Create bound handler
|
||||
this.keyboardHandler = (event) => {
|
||||
// Only handle keyboard events when the viewer is visible
|
||||
if (this.style.display === 'none') return;
|
||||
if (this.style.display === "none") return;
|
||||
|
||||
switch (event.key) {
|
||||
case 'ArrowLeft':
|
||||
case "ArrowLeft":
|
||||
event.preventDefault();
|
||||
this.goToPreviousPage();
|
||||
break;
|
||||
case 'ArrowRight':
|
||||
case "ArrowRight":
|
||||
event.preventDefault();
|
||||
this.goToNextPage();
|
||||
break;
|
||||
case 'Escape':
|
||||
case "Escape":
|
||||
event.preventDefault();
|
||||
this.close();
|
||||
break;
|
||||
@@ -339,10 +341,9 @@ export class SinglePageViewer extends HTMLElement {
|
||||
};
|
||||
|
||||
// Add event listener
|
||||
document.addEventListener('keydown', this.keyboardHandler);
|
||||
document.addEventListener("keydown", this.keyboardHandler);
|
||||
}
|
||||
|
||||
|
||||
// Share current page
|
||||
shareCurrentPage() {
|
||||
if (typeof copyPagePermalink === "function") {
|
||||
@@ -411,11 +412,12 @@ export class SinglePageViewer extends HTMLElement {
|
||||
getAdjacentPages() {
|
||||
// Get all page containers of the same type (main or beilage)
|
||||
let containerSelector;
|
||||
if (this.currentIsBeilage) {
|
||||
containerSelector = '.newspaper-page-container[data-beilage="true"]';
|
||||
} else {
|
||||
containerSelector = ".newspaper-page-container:not([data-beilage])";
|
||||
}
|
||||
containerSelector = ".newspaper-page-container";
|
||||
// if (this.currentIsBeilage) {
|
||||
// containerSelector = '.newspaper-page-container[data-beilage="true"]';
|
||||
// } else {
|
||||
// containerSelector = ".newspaper-page-container:not([data-beilage])";
|
||||
// }
|
||||
|
||||
const pageContainers = Array.from(document.querySelectorAll(containerSelector));
|
||||
console.log(
|
||||
@@ -433,8 +435,7 @@ export class SinglePageViewer extends HTMLElement {
|
||||
console.log("Container page:", pageAttr, "parsed:", pageNum);
|
||||
return pageNum;
|
||||
})
|
||||
.filter((p) => p !== null)
|
||||
.sort((a, b) => a - b);
|
||||
.filter((p) => p !== null);
|
||||
|
||||
console.log("All pages found:", allPages);
|
||||
console.log("Current page:", this.currentPageNumber);
|
||||
@@ -535,9 +536,11 @@ export class SinglePageViewer extends HTMLElement {
|
||||
);
|
||||
|
||||
// Dispatch event for scrollspy to update highlighting
|
||||
document.dispatchEvent(new CustomEvent('singlepageviewer:pagechanged', {
|
||||
detail: { pageNumber: this.currentPageNumber, isBeilage: this.currentIsBeilage }
|
||||
}));
|
||||
document.dispatchEvent(
|
||||
new CustomEvent("singlepageviewer:pagechanged", {
|
||||
detail: { pageNumber: this.currentPageNumber, isBeilage: this.currentIsBeilage },
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -302,29 +302,30 @@ body.page-edition::before {
|
||||
}
|
||||
|
||||
.single-page .newspaper-page-image {
|
||||
max-width: min(400px, 100%);
|
||||
width: 100%;
|
||||
height: auto;
|
||||
width: auto;
|
||||
height: 750px;
|
||||
object-fit: contain;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
/* Larger screens */
|
||||
@media (min-width: 1280px) {
|
||||
.single-page .newspaper-page-image {
|
||||
max-width: min(600px, 100%);
|
||||
height: 900px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Very wide screens */
|
||||
@media (min-width: 1536px) {
|
||||
.single-page .newspaper-page-image {
|
||||
max-width: min(700px, 100%);
|
||||
height: 1050px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Mobile constraints */
|
||||
@media (max-width: 640px) {
|
||||
.single-page .newspaper-page-image {
|
||||
max-width: 100%;
|
||||
height: 600px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user