count ontinuations

This commit is contained in:
Simon Martens
2025-09-17 10:05:19 +02:00
parent 76bd395d23
commit d1bc8db654
13 changed files with 1820 additions and 1742 deletions

231
GEMINI.md Normal file
View File

@@ -0,0 +1,231 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
KGPZ Web is a Go-based web application for serving historical newspaper data from the KGPZ (Königsberger Gelehrte und Politische Zeitung) digital archive. The application combines server-rendered HTML with HTMX for progressive enhancement, providing a modern web interface for browsing historical content.
## Architecture
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)
- **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)
- **Templating**: Custom template engine with Go template integration and helper functions
- **Views**: Frontend assets and templates in `views/` directory
### Key Components
1. **Data Sources**: XML files from Git repository containing historical newspaper metadata
2. **Search**: Full-text search powered by Bleve search engine
3. **External Integrations**: GND (Gemeinsame Normdatei) for person metadata, Geonames for place data
4. **Template System**: Custom engine supporting layouts and partials with embedded filesystem and helper functions
## Development Commands
### Go Backend
```bash
# Run the application in development mode
go run kgpz_web.go
# Build the application
go build -o kgpz_web kgpz_web.go
# Run tests
go test ./helpers/xsdtime/
# Format code
go fmt ./...
# Check for issues
go vet ./...
```
### Frontend Assets (from views/ directory)
```bash
cd views/
# Development server with hot reloading
npm run dev
# Build production assets
npm run build
# Build CSS with Tailwind
npm run tailwind
# Build CSS with PostCSS
npm run css
# Preview built assets
npm run preview
```
## Configuration
The application uses JSON configuration files:
- `config.dev.json` - Development configuration (if exists)
- `config.json` - Default configuration (fallback)
Key configuration options:
- `git_url`: Source repository URL for data
- `git_branch`: Branch to use for data
- `webhook_endpoint`: GitHub webhook endpoint for auto-updates
- `debug`: Enable debug mode and logging
- `watch`: Enable file watching for template hot-reloading
## Data Flow
1. **Startup**: Application clones/pulls Git repository with XML data
2. **Parsing**: XML files parsed into structured models (agents, places, works, issues)
3. **Enrichment**: External APIs (GND, Geonames) enrich metadata
4. **Indexing**: Full-text search index built using Bleve
5. **Serving**: HTTP server serves templated content with HTMX interactions
## Key Dependencies
- **Web Framework**: Fiber (high-performance HTTP framework)
- **Search**: Bleve (full-text search engine)
- **Git Operations**: go-git (Git repository operations)
- **Frontend**: HTMX + Tailwind CSS for progressive enhancement
- **Build Tools**: Vite for asset bundling, PostCSS for CSS processing
## Template Structure
Templates are embedded in the binary:
- **Layouts**: `views/layouts/` - Base page structures
- **Routes**: `views/routes/` - Page-specific templates
- **Assets**: `views/assets/` - Compiled CSS and static files
The template system supports nested layouts and automatic reloading in development mode when `watch: true` is enabled.
## Views Directory Structure
The `views/` directory contains all frontend templates, assets, and build configuration:
### Directory Layout
```
views/
├── layouts/ # Base templates and layouts
│ ├── components/ # Shared layout components (_header, _footer, _menu)
│ └── default/ # Default layout (root.gohtml)
├── routes/ # Page-specific templates
│ ├── akteure/ # Agents/People pages (body.gohtml, head.gohtml)
│ ├── ausgabe/ # Issue pages with components
│ │ └── components/ # Issue-specific components (_inhaltsverzeichnis, _bilder, etc.)
│ ├── components/ # Shared route components (_akteur.gohtml)
│ ├── datenschutz/ # Privacy policy
│ ├── edition/ # Edition pages
│ ├── kategorie/ # Category pages
│ ├── kontakt/ # Contact pages
│ ├── ort/ # Places pages
│ ├── search/ # Search pages
│ └── zitation/ # Citation pages
├── assets/ # Compiled output assets
│ ├── css/ # Compiled CSS files
│ ├── js/ # JavaScript libraries and compiled scripts
│ ├── fonts/ # Font files
│ ├── logo/ # Logo and favicon files
│ └── xslt/ # XSLT transformation files
├── public/ # Static public assets
├── transform/ # Source files for build process
│ ├── main.js # Main JavaScript entry point
│ └── site.css # Source CSS with Tailwind directives
└── node_modules/ # NPM dependencies
```
### Template System
**Layout Templates** (`layouts/`):
- `default/root.gohtml`: Base HTML structure with head, HTMX, Alpine.js setup
- `components/_header.gohtml`: Site header with navigation
- `components/_footer.gohtml`: Site footer
- `components/_menu.gohtml`: Main navigation menu
**Route Templates** (`routes/`):
Each route has dedicated `head.gohtml` and `body.gohtml` files following Go template conventions:
- Pages use German naming: `akteure` (agents), `ausgabe` (issues), `ort` (places), etc.
- Component partials prefixed with `_` (e.g., `_akteur.gohtml`, `_inhaltsverzeichnis.gohtml`)
- HTMX-powered interactions for dynamic content loading
**Template Features**:
- Go template syntax with custom functions from `templating/engine.go`
- Block template inheritance system
- HTMX integration for progressive enhancement
- Conditional development/production asset loading
- Template helper functions for UI components (PageIcon, BeilagePageIcon)
- Pre-processed view models to minimize template logic
### Frontend Assets
**JavaScript Stack**:
- **HTMX**: Core interactivity and AJAX requests
- **Alpine.js**: Lightweight reactivity for UI components
- **Custom Extensions**: HTMX plugins for response targets, client-side templates, loading states
- **Build Tool**: Vite for module bundling and development server
**CSS Stack**:
- **Tailwind CSS v4**: Utility-first CSS framework
- **PostCSS**: CSS processing pipeline
- **RemixIcon**: Icon font library
- **Custom Fonts**: Typography setup in `assets/css/fonts.css`
**Build Process**:
- **Source**: `transform/main.js` and `transform/site.css`
- **Output**: Compiled to `assets/scripts.js` and `assets/style.css`
- **Vite Config**: Production build targeting ES modules
- **PostCSS Config**: Tailwind CSS processing
### Asset Loading Strategy
The root template conditionally loads assets based on environment:
- Development: Uses dev favicon, enables hot reloading
- Production: Optimized assets, production favicon
- Module imports: ES6 modules with `setup()` function from compiled scripts
- Deferred loading: HTMX and Alpine.js loaded with `defer` attribute
## Template Architecture & Best Practices
### View Model Philosophy
The application follows a **logic-in-Go, presentation-in-templates** approach:
- **View Models** (`viewmodels/issue_view.go`): Pre-process all business logic, calculations, and data transformations
- **Templates**: Focus purely on presentation using pre-calculated data
- **Helper Functions** (`templating/engine.go`): Reusable UI components and formatting
### Key View Model Features
- **Pre-calculated metadata**: Page icons, grid layouts, visibility flags
- **Grouped data structures**: Complex relationships resolved in Go
- **Template helpers**: `PageIcon()`, `BeilagePageIcon()` for consistent UI components
### Template Organization
**Ausgabe (Issue) Templates**:
- `body.gohtml`: Main layout structure with conditional rendering
- `components/_inhaltsverzeichnis.gohtml`: Table of contents with pre-processed page data
- `components/_newspaper_layout.gohtml`: Newspaper page grid with absolute positioning
- `components/_bilder.gohtml`: Simple image gallery fallback
- Interactive highlighting system with intersection observer and scroll detection
### JavaScript Integration
- **Progressive Enhancement**: HTMX + Alpine.js for interactivity
- **Real-time Highlighting**: Intersection Observer API with scroll fallback
- **Page Navigation**: Smooth scrolling with visibility detection
- **Responsive Design**: Mobile-optimized with proper touch interactions
## Development Workflow
1. Backend changes: Modify Go files, restart server
2. Template changes: Edit templates in `views/`, automatic reload if watching enabled
3. CSS changes: Run `npm run css` or `npm run tailwind` in views directory
4. JavaScript changes: Edit `transform/main.js`, run `npm run build`
5. Full rebuild: `go build` for backend, `npm run build` for frontend assets
### Adding New Template Logic
1. **First**: Add business logic to view models in Go
2. **Second**: Create reusable template helper functions if needed
3. **Last**: Use pre-processed data in templates for presentation only

View File

@@ -1,39 +1,39 @@
const p = "script[xslt-onload]", a = "xslt-template", u = "xslt-transformed", c = /* @__PURE__ */ new Map();
function m() {
let t = htmx.findAll(p);
for (let e of t)
T(e);
const S = "script[xslt-onload]", w = "xslt-template", T = "xslt-transformed", f = /* @__PURE__ */ new Map();
function b() {
let n = htmx.findAll(S);
for (let t of n)
A(t);
}
function T(t) {
if (t.getAttribute(u) === "true" || !t.hasAttribute(a))
function A(n) {
if (n.getAttribute(T) === "true" || !n.hasAttribute(w))
return;
let e = "#" + t.getAttribute(a), o = c.get(e);
if (!o) {
let n = htmx.find(e);
if (n) {
let l = n.innerHTML ? new DOMParser().parseFromString(n.innerHTML, "application/xml") : n.contentDocument;
o = new XSLTProcessor(), o.importStylesheet(l), c.set(e, o);
let t = "#" + n.getAttribute(w), e = f.get(t);
if (!e) {
let a = htmx.find(t);
if (a) {
let s = a.innerHTML ? new DOMParser().parseFromString(a.innerHTML, "application/xml") : a.contentDocument;
e = new XSLTProcessor(), e.importStylesheet(s), f.set(t, e);
} else
throw new Error("Unknown XSLT template: " + e);
throw new Error("Unknown XSLT template: " + t);
}
let i = new DOMParser().parseFromString(t.innerHTML, "application/xml"), s = o.transformToFragment(i, document), r = new XMLSerializer().serializeToString(s);
t.outerHTML = r;
let o = new DOMParser().parseFromString(n.innerHTML, "application/xml"), i = e.transformToFragment(o, document), r = new XMLSerializer().serializeToString(i);
n.outerHTML = r;
}
function f() {
document.querySelectorAll("template[simple]").forEach((e) => {
let o = e.getAttribute("id"), i = e.content;
function L() {
document.querySelectorAll("template[simple]").forEach((t) => {
let e = t.getAttribute("id"), o = t.content;
customElements.define(
o,
e,
class extends HTMLElement {
constructor() {
super(), this.appendChild(i.cloneNode(!0)), this.slots = this.querySelectorAll("slot");
super(), this.appendChild(o.cloneNode(!0)), this.slots = this.querySelectorAll("slot");
}
connectedCallback() {
let s = [];
let i = [];
this.slots.forEach((r) => {
let n = r.getAttribute("name"), l = this.querySelector(`[slot="${n}"]`);
l && (r.replaceWith(l.cloneNode(!0)), s.push(l));
}), s.forEach((r) => {
let a = r.getAttribute("name"), s = this.querySelector(`[slot="${a}"]`);
s && (r.replaceWith(s.cloneNode(!0)), i.push(s));
}), i.forEach((r) => {
r.remove();
});
}
@@ -41,11 +41,415 @@ function f() {
);
});
}
function d() {
m(), htmx.on("htmx:load", function(t) {
m();
}), f();
window.highlightObserver = window.highlightObserver || null;
window.currentPageContainers = window.currentPageContainers || [];
window.currentActiveIndex = window.currentActiveIndex || 0;
window.pageObserver = window.pageObserver || null;
window.scrollTimeout = window.scrollTimeout || null;
function B() {
window.highlightObserver && (window.highlightObserver.disconnect(), window.highlightObserver = null);
const n = document.querySelectorAll(".newspaper-page-container");
window.highlightObserver = new IntersectionObserver(
(t) => {
x();
},
{
rootMargin: "-20% 0px -70% 0px"
}
), n.forEach((t) => {
window.highlightObserver.observe(t);
});
}
function x() {
const n = [];
document.querySelectorAll(".newspaper-page-container").forEach((e) => {
const o = e.getBoundingClientRect(), i = window.innerHeight, r = Math.max(o.top, 0), a = Math.min(o.bottom, i), s = Math.max(0, a - r), l = o.height, g = s / l >= 0.5, u = e.querySelector("img[data-page]"), m = u ? u.getAttribute("data-page") : "unknown";
g && u && m && !n.includes(m) && n.push(m);
}), q(n), n.length > 0 && E(n);
}
function q(n) {
document.querySelectorAll(".continuation-entry").forEach((t) => {
t.style.display = "none";
}), n.forEach((t) => {
const e = document.querySelector(`[data-page-container="${t}"]`);
e && e.querySelectorAll(".continuation-entry").forEach((i) => {
i.style.display = "";
});
}), k(n), M();
}
function k(n) {
document.querySelectorAll(".work-title").forEach((t) => {
const e = t.getAttribute("data-short-title");
e && (t.textContent = e);
}), n.forEach((t) => {
const e = document.querySelector(`[data-page-container="${t}"]`);
e && e.querySelectorAll(".work-title").forEach((i) => {
const r = i.getAttribute("data-full-title");
r && r !== i.getAttribute("data-short-title") && (i.textContent = r);
});
});
}
function M() {
document.querySelectorAll(".page-entry").forEach((n) => {
const t = n.querySelectorAll(".inhalts-entry");
let e = !1;
t.forEach((o) => {
window.getComputedStyle(o).display !== "none" && (e = !0);
}), e ? n.style.display = "" : n.style.display = "none";
});
}
function C(n) {
E([n]);
}
function E(n) {
console.log("markCurrentPagesInInhaltsverzeichnis called with:", n), document.querySelectorAll("[data-page-container]").forEach((e) => {
e.hasAttribute("data-beilage") ? (e.classList.remove("border-red-500"), e.classList.add("border-amber-400")) : (e.classList.remove("border-red-500"), e.classList.add("border-slate-300"));
}), document.querySelectorAll(".page-number-inhalts").forEach((e) => {
e.classList.remove("text-red-600", "font-bold"), e.classList.add("text-slate-700", "font-semibold"), e.style.textDecoration = "", e.style.pointerEvents = "", e.classList.contains("bg-blue-50") ? e.classList.add("hover:bg-blue-100") : e.classList.contains("bg-amber-50") && e.classList.add("hover:bg-amber-100"), !e.classList.contains("bg-amber-50") && !e.classList.contains("bg-blue-50") && e.classList.add("bg-blue-50");
}), document.querySelectorAll(".inhalts-entry").forEach((e) => {
e.classList.add("hover:bg-slate-100"), e.style.cursor = "";
}), document.querySelectorAll('.inhalts-entry a[href*="/"]').forEach((e) => {
e.classList.remove("no-underline"), e.classList.contains("bg-blue-50") && e.classList.add("hover:bg-blue-100");
});
const t = [];
n.forEach((e) => {
const o = document.querySelector(
`.page-number-inhalts[data-page-number="${e}"]`
);
if (o) {
o.classList.remove(
"text-slate-700",
"hover:bg-blue-100",
"hover:bg-amber-100"
), o.classList.add("text-red-600", "font-bold"), o.style.textDecoration = "none", o.style.pointerEvents = "none", t.push(o);
const i = document.querySelector(`[data-page-container="${e}"]`);
i && (i.classList.remove("border-slate-300", "border-amber-400"), i.classList.add("border-red-500"));
const r = o.closest(".page-entry");
r && (r.querySelectorAll(".inhalts-entry").forEach((s) => {
s.classList.remove("hover:bg-slate-100"), s.style.cursor = "default";
}), r.querySelectorAll('a[href*="/"]').forEach((s) => {
s.getAttribute("aria-current") === "page" && (s.style.textDecoration = "none", s.style.pointerEvents = "none", s.classList.add("no-underline"), s.classList.remove("hover:bg-blue-100"));
}));
}
}), t.length > 0 && H(t[0]), document.querySelectorAll(".page-indicator").forEach((e) => {
e.classList.remove("text-red-600", "font-bold"), e.classList.add("text-slate-600", "font-semibold"), e.classList.contains("bg-amber-50") || e.classList.add("bg-blue-50");
}), n.forEach((e) => {
const o = document.querySelector(`.page-indicator[data-page="${e}"]`);
o && (o.classList.remove("text-slate-600"), o.classList.add("text-red-600", "font-bold"));
});
}
function H(n) {
const t = n.closest(".lg\\:overflow-y-auto");
if (t) {
const e = t.getBoundingClientRect(), o = n.getBoundingClientRect(), i = o.top < e.top, r = o.bottom > e.bottom;
(i || r) && n.scrollIntoView({
behavior: "smooth",
block: "center"
});
}
}
function R(n, t, e) {
const o = document.getElementById("pageModal"), i = document.getElementById("modalImage");
i.src = n.src, i.alt = n.alt, o.classList.remove("hidden"), C(t);
}
function I() {
document.getElementById("pageModal").classList.add("hidden");
}
function $() {
if (window.pageObserver && (window.pageObserver.disconnect(), window.pageObserver = null), window.currentPageContainers = Array.from(
document.querySelectorAll(".newspaper-page-container")
), window.currentActiveIndex = 0, h(), document.querySelector(".newspaper-page-container")) {
let t = /* @__PURE__ */ new Set();
window.pageObserver = new IntersectionObserver(
(e) => {
if (e.forEach((o) => {
const i = window.currentPageContainers.indexOf(o.target);
i !== -1 && (o.isIntersecting ? t.add(i) : t.delete(i));
}), t.size > 0) {
const i = Array.from(t).sort((r, a) => r - a)[0];
i !== window.currentActiveIndex && (window.currentActiveIndex = i, h());
}
},
{
rootMargin: "-20% 0px -70% 0px"
}
), window.currentPageContainers.forEach((e) => {
window.pageObserver.observe(e);
});
}
}
function O() {
if (window.currentActiveIndex > 0) {
let n = -1;
const t = [];
window.currentPageContainers.forEach((o, i) => {
const r = o.getBoundingClientRect(), a = window.innerHeight, s = Math.max(r.top, 0), l = Math.min(r.bottom, a), d = Math.max(0, l - s), g = r.height;
d / g >= 0.3 && t.push(i);
});
const e = Math.min(...t);
for (let o = e - 1; o >= 0; o--)
if (!t.includes(o)) {
n = o;
break;
}
n === -1 && e > 0 && (n = e - 1), n >= 0 && (window.currentActiveIndex = n, window.currentPageContainers[window.currentActiveIndex].scrollIntoView({
behavior: "smooth",
block: "start"
}), setTimeout(() => {
h();
}, 100));
}
}
function K() {
if (window.currentActiveIndex < window.currentPageContainers.length - 1) {
let n = -1;
const t = [];
window.currentPageContainers.forEach((o, i) => {
const r = o.getBoundingClientRect(), a = window.innerHeight, s = Math.max(r.top, 0), l = Math.min(r.bottom, a), d = Math.max(0, l - s), g = r.height;
d / g >= 0.3 && t.push(i);
});
const e = Math.max(...t);
for (let o = e + 1; o < window.currentPageContainers.length; o++)
if (!t.includes(o)) {
n = o;
break;
}
n === -1 && e < window.currentPageContainers.length - 1 && (n = e + 1), n >= 0 && n < window.currentPageContainers.length && (window.currentActiveIndex = n, window.currentPageContainers[window.currentActiveIndex].scrollIntoView({
behavior: "smooth",
block: "start"
}), setTimeout(() => {
h();
}, 100));
}
}
function V() {
if (P()) {
const t = document.querySelector("#newspaper-content .newspaper-page-container");
t && t.scrollIntoView({
behavior: "smooth",
block: "start"
});
} else {
const t = document.querySelector('[class*="border-t-2 border-amber-200"]');
t && t.scrollIntoView({
behavior: "smooth",
block: "start"
});
}
}
function P() {
const n = [];
window.currentPageContainers.forEach((t, e) => {
const o = t.getBoundingClientRect(), i = window.innerHeight, r = Math.max(o.top, 0), a = Math.min(o.bottom, i), s = Math.max(0, a - r), l = o.height;
s / l >= 0.3 && n.push(e);
});
for (const t of n) {
const e = window.currentPageContainers[t];
if (e && e.id && e.id.includes("beilage-"))
return !0;
}
return !1;
}
function h() {
const n = document.getElementById("prevPageBtn"), t = document.getElementById("nextPageBtn"), e = document.getElementById("beilageBtn");
if (n && (window.currentActiveIndex <= 0 ? n.style.display = "none" : n.style.display = "flex"), t && (window.currentActiveIndex >= window.currentPageContainers.length - 1 ? t.style.display = "none" : t.style.display = "flex"), e) {
const o = P(), i = e.querySelector("i");
o ? (e.title = "Zur Hauptausgabe", e.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", i && (i.className = "ri-file-text-line text-lg lg:text-xl")) : (e.title = "Zu Beilage", e.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", i && (i.className = "ri-attachment-line text-lg lg:text-xl"));
}
}
function D() {
const n = document.getElementById("shareLinkBtn");
let t = "";
if (window.currentActiveIndex !== void 0 && window.currentPageContainers && window.currentPageContainers[window.currentActiveIndex]) {
const i = window.currentPageContainers[window.currentActiveIndex].querySelector("[data-page]");
i && (t = `#page-${i.getAttribute("data-page")}`);
}
const e = window.location.origin + window.location.pathname + t;
navigator.share ? navigator.share({
title: document.title,
url: e
}).catch((o) => {
y(e, n);
}) : y(e, n);
}
function y(n, t) {
if (navigator.clipboard)
navigator.clipboard.writeText(n).then(() => {
c(t, "Link kopiert!");
}).catch((e) => {
c(t, "Kopieren fehlgeschlagen");
});
else {
const e = document.createElement("textarea");
e.value = n, document.body.appendChild(e), e.select();
try {
const o = document.execCommand("copy");
c(t, o ? "Link kopiert!" : "Kopieren fehlgeschlagen");
} catch {
c(t, "Kopieren fehlgeschlagen");
} finally {
document.body.removeChild(e);
}
}
}
function Z() {
const n = document.getElementById("citationBtn"), t = document.title || "KGPZ", e = window.location.href, o = (/* @__PURE__ */ new Date()).toLocaleDateString("de-DE"), i = `Königsberger Gelehrte und Politische Zeitung (KGPZ). ${t}. Digital verfügbar unter: ${e} (Zugriff: ${o}).`;
if (navigator.clipboard)
navigator.clipboard.writeText(i).then(() => {
c(n, "Zitation kopiert!");
}).catch((r) => {
c(n, "Kopieren fehlgeschlagen");
});
else {
const r = document.createElement("textarea");
r.value = i, document.body.appendChild(r), r.select();
try {
const a = document.execCommand("copy");
c(n, a ? "Zitation kopiert!" : "Kopieren fehlgeschlagen");
} catch {
c(n, "Kopieren fehlgeschlagen");
} finally {
document.body.removeChild(r);
}
}
}
function c(n, t) {
const e = document.querySelector(".simple-popup");
e && e.remove();
const o = document.createElement("div");
o.className = "simple-popup", o.textContent = t, o.style.cssText = `
position: fixed;
background: #374151;
color: white;
padding: 6px 12px;
border-radius: 6px;
font-size: 13px;
font-weight: 500;
z-index: 1000;
pointer-events: none;
opacity: 0;
transition: opacity 0.2s ease;
white-space: nowrap;
`;
const i = n.getBoundingClientRect(), r = window.innerHeight, a = window.innerWidth;
let s = i.left - 10, l = i.bottom + 8;
const d = 120, g = 32;
s + d > a && (s = i.right - d + 10), l + g > r && (l = i.top - g - 8), o.style.left = Math.max(5, s) + "px", o.style.top = Math.max(5, l) + "px", document.body.appendChild(o), setTimeout(() => {
o.style.opacity = "1";
}, 10), setTimeout(() => {
o.style.opacity = "0", setTimeout(() => {
o.parentNode && o.remove();
}, 200);
}, 2e3);
}
function v() {
const n = window.location.hash;
let t = "", e = null;
if (n.startsWith("#page-")) {
if (t = n.replace("#page-", ""), e = document.getElementById(`page-${t}`), !e) {
const o = document.querySelectorAll(".newspaper-page-container[data-pages]");
for (const i of o) {
const r = i.getAttribute("data-pages");
if (r && r.split(",").includes(t)) {
e = i;
break;
}
}
}
e || (e = document.getElementById(`beilage-1-page-${t}`) || document.getElementById(`beilage-2-page-${t}`) || document.querySelector(`[id*="beilage"][id*="page-${t}"]`));
} else if (n.startsWith("#beilage-")) {
const o = n.match(/#beilage-(\d+)-page-(\d+)/);
if (o) {
const i = o[1];
t = o[2], e = document.getElementById(`beilage-${i}-page-${t}`);
}
}
e && t && setTimeout(() => {
e.scrollIntoView({
behavior: "smooth",
block: "start"
}), C(t);
}, 300);
}
function z(n, t, e = !1) {
let o = "";
e ? o = `#beilage-1-page-${n}` : o = `#page-${n}`;
const i = window.location.origin + window.location.pathname + o;
if (navigator.clipboard)
navigator.clipboard.writeText(i).then(() => {
c(t, "Link kopiert!");
}).catch((r) => {
c(t, "Kopieren fehlgeschlagen");
});
else {
const r = document.createElement("textarea");
r.value = i, document.body.appendChild(r), r.select();
try {
const a = document.execCommand("copy");
c(t, a ? "Link kopiert!" : "Kopieren fehlgeschlagen");
} catch {
c(t, "Kopieren fehlgeschlagen");
} finally {
document.body.removeChild(r);
}
}
}
function _(n, t) {
const e = document.title || "KGPZ", o = `${window.location.origin}${window.location.pathname}#page-${n}`, i = (/* @__PURE__ */ new Date()).toLocaleDateString("de-DE"), r = `Königsberger Gelehrte und Politische Zeitung (KGPZ). ${e}, Seite ${n}. Digital verfügbar unter: ${o} (Zugriff: ${i}).`;
if (navigator.clipboard)
navigator.clipboard.writeText(r).then(() => {
c(t, "Zitation kopiert!");
}).catch((a) => {
c(t, "Kopieren fehlgeschlagen");
});
else {
const a = document.createElement("textarea");
a.value = r, document.body.appendChild(a), a.select();
try {
const s = document.execCommand("copy");
c(t, s ? "Zitation kopiert!" : "Kopieren fehlgeschlagen");
} catch {
c(t, "Kopieren fehlgeschlagen");
} finally {
document.body.removeChild(a);
}
}
}
function p() {
B(), $(), window.addEventListener("scroll", function() {
clearTimeout(window.scrollTimeout), window.scrollTimeout = setTimeout(() => {
x(), h();
}, 50);
}), v(), window.addEventListener("hashchange", v), document.addEventListener("keydown", function(n) {
n.key === "Escape" && I();
});
}
window.enlargePage = R;
window.closeModal = I;
window.scrollToPreviousPage = O;
window.scrollToNextPage = K;
window.scrollToBeilage = V;
window.shareCurrentPage = D;
window.generateCitation = Z;
window.copyPagePermalink = z;
window.generatePageCitation = _;
function F() {
b(), L(), document.querySelector(".newspaper-page-container") && p(), htmx.on("htmx:load", function(n) {
b();
}), document.body.addEventListener("htmx:afterSwap", function(n) {
setTimeout(() => {
document.querySelector(".newspaper-page-container") && p();
}, 100);
}), document.body.addEventListener("htmx:afterSettle", function(n) {
setTimeout(() => {
document.querySelector(".newspaper-page-container") && p();
}, 200);
}), document.body.addEventListener("htmx:load", function(n) {
setTimeout(() => {
document.querySelector(".newspaper-page-container") && p();
}, 100);
});
}
export {
d as setup
F as setup
};

File diff suppressed because one or more lines are too long

View File

@@ -1,41 +0,0 @@
{{ $model := .model }}
{{ $images := $model.Images }}
{{ if $images.HasImages }}
<div class="mt-6">
<h3 class="text-lg font-medium mb-4">Seiten der Ausgabe</h3>
{{- if $images.MainPages }}
<div class="mb-6">
<h4 class="text-md font-medium mb-2">Hauptausgabe</h4>
<div class="grid grid-cols-2 gap-4">
{{- range $images.MainPages -}}
{{- if .Available -}}
<div class="border rounded-lg p-2">
<h5 class="font-bold text-slate-700 bg-blue-50 px-2 py-1 rounded text-sm mb-2">Seite {{ .PageNumber }}</h5>
<img src="{{ .ImagePath }}" alt="Seite {{ .PageNumber }}" class="w-full h-auto border" loading="lazy">
</div>
{{- end -}}
{{- end -}}
</div>
</div>
{{- end }}
{{- range $beilageNum, $pages := $images.AdditionalPages -}}
{{- if $pages }}
<div class="mb-6">
<h4 class="text-md font-medium mb-2">Beilage {{ $beilageNum }}</h4>
<div class="grid grid-cols-2 gap-4">
{{- range $pages -}}
{{- if .Available -}}
<div class="border rounded-lg p-2">
<h5 class="font-bold text-slate-700 bg-blue-50 px-2 py-1 rounded text-sm mb-2">Seite {{ .PageNumber }}</h5>
<img src="{{ .ImagePath }}" alt="Beilage {{ $beilageNum }}, Seite {{ .PageNumber }}" class="w-full h-auto border" loading="lazy">
</div>
{{- end -}}
{{- end -}}
</div>
</div>
{{- end -}}
{{- end -}}
</div>
{{ end }}

View File

@@ -1,579 +0,0 @@
{{ $model := .model }}
<div class="w-full min-h-screen">
<!-- Navigation Bar -->
<div class="sticky top-0 z-40 bg-white border-b border-gray-200 shadow-sm mb-6">
<div class="max-w-7xl mx-auto px-4 py-4">
<div class="flex items-center justify-between">
<div class="flex items-center space-x-4">
{{ template "_title_nav" . }}
</div>
<div class="flex items-center space-x-3">
<!-- Navigation buttons -->
<button onclick="scrollToPreviousPage()" class="flex items-center px-3 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-md transition-colors">
<i class="ri-arrow-up-line mr-1"></i>
Vorherige Seite
</button>
<button onclick="scrollToNextPage()" class="flex items-center px-3 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-md transition-colors">
<i class="ri-arrow-down-line mr-1"></i>
Nächste Seite
</button>
{{ if $model.Images.AdditionalPages }}
<button onclick="scrollToBeilage()" class="flex items-center px-3 py-2 bg-amber-600 hover:bg-amber-700 text-white rounded-md transition-colors">
<i class="ri-attachment-line mr-1"></i>
Zu Beilage
</button>
{{ end }}
<!-- Switch back to sidebar layout -->
<a href="?layout=sidebar" class="flex items-center px-3 py-2 bg-gray-600 hover:bg-gray-700 text-white rounded-md transition-colors">
<i class="ri-sidebar-unfold-line mr-1"></i>
Seitenleiste
</a>
</div>
</div>
</div>
</div>
<div class="max-w-7xl mx-auto px-4">
<div class="flex flex-col lg:flex-row gap-6 w-full">
<!-- Left side: Collapsible Inhaltsverzeichnis -->
<div class="lg:w-1/4 xl:w-1/5 flex-shrink-0">
<div class="lg:sticky lg:top-24 lg:max-h-[calc(100vh-6rem)] lg:overflow-y-auto">
{{ template "_inhaltsverzeichnis" . }}
</div>
</div>
<!-- Right side: Full-width Newspaper pages -->
<div class="lg:w-3/4 xl:w-4/5 flex-1">
{{ template "_newspaper_layout" . }}
</div>
</div>
</div>
</div>
<script>
let currentPageContainers = [];
let currentActiveIndex = 0;
// Initialize page tracking for full-width layout
document.addEventListener('DOMContentLoaded', function() {
// Get all page containers
currentPageContainers = document.querySelectorAll('.newspaper-page-container');
// Set up intersection observer for active page tracking
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
// Check if this is a double-spread container
const doubleSpread = entry.target.querySelector('.double-spread');
if (doubleSpread) {
// Handle double-spread: highlight both pages
const pageImages = entry.target.querySelectorAll('img[data-page]');
const pageNumbers = Array.from(pageImages).map(img => img.getAttribute('data-page'));
markCurrentPagesInInhaltsverzeichnis(pageNumbers);
} else {
// Handle single page
const pageImg = entry.target.querySelector('img[data-page]');
if (pageImg) {
const pageNumber = pageImg.getAttribute('data-page');
markCurrentPageInInhaltsverzeichnis(pageNumber);
}
}
// Update current active index
const containerIndex = Array.from(currentPageContainers).indexOf(entry.target);
if (containerIndex !== -1) {
currentActiveIndex = containerIndex;
}
}
});
}, {
rootMargin: '-20% 0px -70% 0px' // Trigger when page is mostly in view
});
// Observe all page containers
currentPageContainers.forEach(container => {
observer.observe(container);
});
});
function scrollToPreviousPage() {
if (currentActiveIndex > 0) {
currentActiveIndex--;
currentPageContainers[currentActiveIndex].scrollIntoView({
behavior: 'smooth',
block: 'start'
});
}
}
function scrollToNextPage() {
if (currentActiveIndex < currentPageContainers.length - 1) {
currentActiveIndex++;
currentPageContainers[currentActiveIndex].scrollIntoView({
behavior: 'smooth',
block: 'start'
});
}
}
function scrollToBeilage() {
const beilageElement = document.querySelector('[id^="beilage-"]');
if (beilageElement) {
beilageElement.scrollIntoView({
behavior: 'smooth',
block: 'start'
});
}
}
function markCurrentPageInInhaltsverzeichnis(pageNumber) {
markCurrentPagesInInhaltsverzeichnis([pageNumber]);
}
function markCurrentPagesInInhaltsverzeichnis(pageNumbers) {
// Reset all page container borders to default
document.querySelectorAll('[data-page-container]').forEach(container => {
if (container.hasAttribute('data-beilage')) {
container.classList.remove('border-red-500');
container.classList.add('border-amber-400');
} else {
container.classList.remove('border-red-500');
container.classList.add('border-slate-300');
}
});
// Hide all continuation entries by default
document.querySelectorAll('.continuation-entry').forEach(entry => {
entry.classList.add('hidden');
});
// Reset all page numbers in Inhaltsverzeichnis
document.querySelectorAll('.page-number-inhalts').forEach(pageNum => {
pageNum.classList.remove('bg-red-500', 'text-white');
pageNum.classList.add('text-slate-700');
pageNum.style.textDecoration = '';
pageNum.style.pointerEvents = '';
// Restore hover effects
if (pageNum.classList.contains('bg-blue-50')) {
pageNum.classList.add('hover:bg-blue-100');
} else if (pageNum.classList.contains('bg-amber-50')) {
pageNum.classList.add('hover:bg-amber-100');
}
// Restore original background colors
if (pageNum.classList.contains('bg-amber-50')) {
// Keep amber background for Beilage pages
} else {
pageNum.classList.remove('bg-amber-50');
pageNum.classList.add('bg-blue-50');
}
});
// Reset all containers and links in Inhaltsverzeichnis
document.querySelectorAll('.inhalts-entry').forEach(container => {
container.classList.add('hover:bg-slate-100');
container.style.cursor = '';
});
document.querySelectorAll('.inhalts-entry .author-link').forEach(link => {
link.style.textDecoration = '';
link.style.pointerEvents = '';
link.classList.remove('no-underline');
});
document.querySelectorAll('.inhalts-entry a[href*="/"]').forEach(link => {
link.style.textDecoration = '';
link.style.pointerEvents = '';
link.classList.remove('no-underline');
if (link.classList.contains('bg-blue-50')) {
link.classList.add('hover:bg-blue-100');
}
});
// Find and highlight the current page numbers
const highlightedElements = [];
const highlightedRanges = new Set(); // Track which ranges we've already highlighted
pageNumbers.forEach(pageNumber => {
// Convert pageNumber to integer for comparison
const currentPageNum = parseInt(pageNumber);
// Look for all entries that should be highlighted for this page
const allPageNumbers = document.querySelectorAll('.page-number-inhalts');
for (const pageNumElement of allPageNumbers) {
const startPage = parseInt(pageNumElement.getAttribute('data-page-number'));
const endPage = parseInt(pageNumElement.getAttribute('data-end-page'));
const rangeKey = `${startPage}-${endPage}`;
// Check if this page falls within this range
if (currentPageNum >= startPage && currentPageNum <= endPage) {
// Only highlight this range once, even if multiple visible pages fall within it
if (!highlightedRanges.has(rangeKey)) {
pageNumElement.classList.remove('bg-blue-50', 'bg-amber-50', 'text-slate-700', 'hover:bg-blue-100', 'hover:bg-amber-100');
pageNumElement.classList.add('bg-red-500', 'text-white');
pageNumElement.style.textDecoration = 'none';
pageNumElement.style.pointerEvents = 'none';
highlightedElements.push(pageNumElement);
highlightedRanges.add(rangeKey);
// Highlight the page container's left border
const pageContainer = document.querySelector(`[data-page-container="${startPage}"]`);
if (pageContainer) {
pageContainer.classList.remove('border-slate-300', 'border-amber-400');
pageContainer.classList.add('border-red-500');
// Show continuation entries for this visible page
const continuationEntries = pageContainer.querySelectorAll('.continuation-entry[data-page="' + startPage + '"]');
continuationEntries.forEach(entry => {
entry.classList.remove('hidden');
});
}
// Also make links in the current article non-clickable and remove hover effects
const parentEntry = pageNumElement.closest('.mb-4');
if (parentEntry) {
// Remove hover effects from the container
const entryContainers = parentEntry.querySelectorAll('.inhalts-entry');
entryContainers.forEach(container => {
container.classList.remove('hover:bg-slate-100');
container.style.cursor = 'default';
});
// Make all links non-clickable and remove underlines
parentEntry.querySelectorAll('.author-link').forEach(link => {
link.style.textDecoration = 'none';
link.style.pointerEvents = 'none';
link.classList.add('no-underline');
});
// Also handle issue reference links
parentEntry.querySelectorAll('a[href*="/"]').forEach(link => {
if (link.getAttribute('aria-current') === 'page') {
link.style.textDecoration = 'none';
link.style.pointerEvents = 'none';
link.classList.add('no-underline');
link.classList.remove('hover:bg-blue-100');
}
});
}
}
}
}
});
// Auto-scroll to first highlighted element if it exists
if (highlightedElements.length > 0) {
scrollToHighlightedPage(highlightedElements[0]);
}
// Also highlight page indicators
document.querySelectorAll('.page-indicator').forEach(indicator => {
indicator.classList.remove('bg-red-500', 'text-white');
indicator.classList.add('bg-blue-50', 'text-slate-600');
});
// Highlight page indicators for all current pages
pageNumbers.forEach(pageNumber => {
const pageIndicator = document.querySelector(`.page-indicator[data-page="${pageNumber}"]`);
if (pageIndicator) {
pageIndicator.classList.remove('bg-blue-50', 'bg-green-50', 'bg-amber-50', 'text-slate-600');
pageIndicator.classList.add('bg-red-500', 'text-white');
}
});
}
function scrollToHighlightedPage(element) {
// Check if the element is in a scrollable container
const inhaltsContainer = element.closest('.lg\\:overflow-y-auto');
if (inhaltsContainer) {
// Calculate position
const containerRect = inhaltsContainer.getBoundingClientRect();
const elementRect = element.getBoundingClientRect();
// Check if element is not fully visible
const isAboveContainer = elementRect.top < containerRect.top;
const isBelowContainer = elementRect.bottom > containerRect.bottom;
if (isAboveContainer || isBelowContainer) {
// Scroll to make element visible with some padding
element.scrollIntoView({
behavior: 'smooth',
block: 'center'
});
}
}
}
// Keyboard navigation
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
// Close modal if open
const modal = document.getElementById('pageModal');
if (modal && !modal.classList.contains('hidden')) {
modal.classList.add('hidden');
}
} else if (e.key === 'ArrowUp' || e.key === 'PageUp') {
e.preventDefault();
scrollToPreviousPage();
} else if (e.key === 'ArrowDown' || e.key === 'PageDown') {
e.preventDefault();
scrollToNextPage();
}
});
function shareCurrentPage() {
const button = document.getElementById('shareLinkBtn');
// Get current page information
let pageInfo = '';
// Try to get the currently visible page number from active containers
if (window.currentActiveIndex !== undefined && window.currentPageContainers && window.currentPageContainers[window.currentActiveIndex]) {
const activeContainer = window.currentPageContainers[window.currentActiveIndex];
const pageElement = activeContainer.querySelector('[data-page]');
if (pageElement) {
const pageNumber = pageElement.getAttribute('data-page');
pageInfo = `#page-${pageNumber}`;
}
}
// Construct the shareable URL
const currentUrl = window.location.origin + window.location.pathname + pageInfo;
// Try to use Web Share API if available (mobile browsers)
if (navigator.share) {
navigator.share({
title: document.title,
url: currentUrl
}).catch(err => {
console.log('Error sharing:', err);
// Fallback to clipboard
copyToClipboard(currentUrl, button);
});
} else {
// Fallback: copy to clipboard
copyToClipboard(currentUrl, button);
}
}
function copyToClipboard(text, button) {
if (navigator.clipboard) {
navigator.clipboard.writeText(text).then(() => {
showSimplePopup(button, 'Link kopiert!');
}).catch(err => {
showSimplePopup(button, 'Kopieren fehlgeschlagen');
});
} else {
// Fallback for older browsers
const textarea = document.createElement('textarea');
textarea.value = text;
document.body.appendChild(textarea);
textarea.select();
try {
const successful = document.execCommand('copy');
showSimplePopup(button, successful ? 'Link kopiert!' : 'Kopieren fehlgeschlagen');
} catch (err) {
showSimplePopup(button, 'Kopieren fehlgeschlagen');
} finally {
document.body.removeChild(textarea);
}
}
}
function generateCitation() {
const button = document.getElementById('citationBtn');
// Get current page and issue information
const issueInfo = document.title || 'KGPZ';
const currentUrl = window.location.href;
// Basic citation format (can be expanded later)
const currentDate = new Date().toLocaleDateString('de-DE');
const citation = `Königsberger Gelehrte und Politische Zeitung (KGPZ). ${issueInfo}. Digital verfügbar unter: ${currentUrl} (Zugriff: ${currentDate}).`;
// Copy to clipboard
if (navigator.clipboard) {
navigator.clipboard.writeText(citation).then(() => {
showSimplePopup(button, 'Zitation kopiert!');
}).catch(err => {
showSimplePopup(button, 'Kopieren fehlgeschlagen');
});
} else {
// Fallback for older browsers
const textarea = document.createElement('textarea');
textarea.value = citation;
document.body.appendChild(textarea);
textarea.select();
try {
const successful = document.execCommand('copy');
showSimplePopup(button, successful ? 'Zitation kopiert!' : 'Kopieren fehlgeschlagen');
} catch (err) {
showSimplePopup(button, 'Kopieren fehlgeschlagen');
} finally {
document.body.removeChild(textarea);
}
}
}
function showSimplePopup(button, message) {
// Remove any existing popup
const existingPopup = document.querySelector('.simple-popup');
if (existingPopup) {
existingPopup.remove();
}
// Create popup element
const popup = document.createElement('div');
popup.className = 'simple-popup';
popup.textContent = message;
// Style the popup
popup.style.cssText = `
position: fixed;
background: #374151;
color: white;
padding: 6px 12px;
border-radius: 6px;
font-size: 13px;
font-weight: 500;
z-index: 1000;
pointer-events: none;
opacity: 0;
transition: opacity 0.2s ease;
white-space: nowrap;
`;
// Position popup next to button
const buttonRect = button.getBoundingClientRect();
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft;
popup.style.left = (buttonRect.left + scrollLeft - 10) + 'px';
popup.style.top = (buttonRect.bottom + scrollTop + 8) + 'px';
// Add to page
document.body.appendChild(popup);
// Fade in
setTimeout(() => {
popup.style.opacity = '1';
}, 10);
// Auto-remove after 2 seconds
setTimeout(() => {
popup.style.opacity = '0';
setTimeout(() => {
if (popup.parentNode) {
popup.remove();
}
}, 200);
}, 2000);
}
// Function to copy page permalink
function copyPagePermalink(pageNumber, button, isBeilage = false) {
let pageFragment = '';
if (isBeilage) {
pageFragment = `#beilage-1-page-${pageNumber}`;
} else {
pageFragment = `#page-${pageNumber}`;
}
const currentUrl = window.location.origin + window.location.pathname + pageFragment;
// Copy to clipboard
if (navigator.clipboard) {
navigator.clipboard.writeText(currentUrl).then(() => {
showSimplePopup(button, 'Link kopiert!');
}).catch(err => {
showSimplePopup(button, 'Kopieren fehlgeschlagen');
});
} else {
// Fallback for older browsers
const textarea = document.createElement('textarea');
textarea.value = currentUrl;
document.body.appendChild(textarea);
textarea.select();
try {
const successful = document.execCommand('copy');
showSimplePopup(button, successful ? 'Link kopiert!' : 'Kopieren fehlgeschlagen');
} catch (err) {
showSimplePopup(button, 'Kopieren fehlgeschlagen');
} finally {
document.body.removeChild(textarea);
}
}
}
// Handle hash navigation to scroll to specific pages
function scrollToPageFromHash() {
const hash = window.location.hash;
let pageNumber = '';
let targetContainer = null;
if (hash.startsWith('#page-')) {
pageNumber = hash.replace('#page-', '');
// Try to find exact page container first
targetContainer = document.getElementById(`page-${pageNumber}`);
// If not found, try to find container that contains this page
if (!targetContainer) {
// Look for double-spread containers that contain this page
const containers = document.querySelectorAll('.newspaper-page-container[data-pages]');
for (const container of containers) {
const pages = container.getAttribute('data-pages');
if (pages && pages.split(',').includes(pageNumber)) {
targetContainer = container;
break;
}
}
}
// If still not found, try beilage containers
if (!targetContainer) {
targetContainer = document.getElementById(`beilage-1-page-${pageNumber}`) ||
document.getElementById(`beilage-2-page-${pageNumber}`) ||
document.querySelector(`[id*="beilage"][id*="page-${pageNumber}"]`);
}
} else if (hash.startsWith('#beilage-')) {
// Handle beilage-specific hashes like #beilage-1-page-101
const match = hash.match(/#beilage-(\d+)-page-(\d+)/);
if (match) {
const beilageNum = match[1];
pageNumber = match[2];
targetContainer = document.getElementById(`beilage-${beilageNum}-page-${pageNumber}`);
}
}
if (targetContainer && pageNumber) {
setTimeout(() => {
targetContainer.scrollIntoView({
behavior: 'smooth',
block: 'start'
});
// Highlight the specific page in the table of contents
markCurrentPageInInhaltsverzeichnis(pageNumber);
}, 300);
}
}
// Function to copy page permalink
function copyPagePermalink(pageNumber, button, isBeilage = false) {
let pageFragment = '';
if (isBeilage) {
pageFragment = `#beilage-1-page-${pageNumber}`;
} else {
pageFragment = `#page-${pageNumber}`;
}
const currentUrl = window.location.origin + window.location.pathname + pageFragment;
copyToClipboardWithMessage(currentUrl, button, 'Link kopiert!');
}
// Initialize hash handling
document.addEventListener('DOMContentLoaded', scrollToPageFromHash);
window.addEventListener('hashchange', scrollToPageFromHash);
</script>

View File

@@ -13,13 +13,20 @@
{{ $firstItem := "" }}
{{ if $pageItems }}{{ $firstItem = index $pageItems 0 }}{{ end }}
<!-- Individual page entry -->
<div
class="mb-6 first:mb-0 pl-4 border-l-4 border-slate-300 page-entry"
data-page-container="{{ $page }}">
<div class="flex items-center justify-between gap-2 mb-2">
<div class="flex items-center gap-2">
<span class="icon-container">{{ if $firstItem }}{{ PageIcon $firstItem.PageIcon }}{{ else }}{{ PageIcon "first" }}{{ end }}</span>
<span class="icon-container"
>{{ if $firstItem }}
{{ PageIcon $firstItem.PageIcon }}
{{ else }}
{{ PageIcon "first" }}
{{ end }}</span
>
<a
href="#page-{{ $page }}"
class="page-number-inhalts font-bold text-slate-700 bg-slate-100 px-2 py-1 rounded text-sm transition-colors duration-200 hover:bg-slate-200 no-underline"
@@ -40,50 +47,68 @@
<div class="space-y-0">
{{ if $pageItems }}
{{ range $individualPiece := $pageItems }}
<div
class="inhalts-entry py-1 px-0 bg-slate-50 rounded hover:bg-slate-100 transition-colors duration-200 {{ if $individualPiece.PieceByIssue.IsContinuation }}
continuation-entry
{{ end }}"
data-page="{{ $page }}"
{{ if $individualPiece.PieceByIssue.IsContinuation }}
data-is-continuation="true"
{{ end }}
style="{{ if $individualPiece.PieceByIssue.IsContinuation }}
display: none;
{{ end }}">
{{ template "_inhaltsverzeichnis_eintrag" $individualPiece.PieceByIssue }}
<div
class="inhalts-entry py-1 px-0 bg-slate-50 rounded hover:bg-slate-100 transition-colors duration-200 {{ if $individualPiece.PieceByIssue.IsContinuation }}
continuation-entry
{{ end }}"
data-page="{{ $page }}"
{{ if $individualPiece.PieceByIssue.IsContinuation }}
data-is-continuation="true"
{{ end }}
style="{{ if $individualPiece.PieceByIssue.IsContinuation }}
display: none;
{{ end }}">
{{ template "_inhaltsverzeichnis_eintrag" $individualPiece.PieceByIssue }}
<!-- Links zu anderen Teilen: -->
{{ if and (not $individualPiece.PieceByIssue.IsContinuation) (gt (len $individualPiece.IssueRefs) 1) }}
<div class="mt-1 pt-1 border-t border-slate-100">
<div class="flex items-center flex-wrap gap-1">
<i class="ri-links-line text-slate-600 text-sm mr-1"></i>
{{ range $issue := $individualPiece.IssueRefs }}
<a
href="/{{- $issue.When -}}/{{- $issue.Nr -}}{{- if $issue.Von -}}
{{- if $issue.Beilage -}}
#beilage-{{ $issue.Beilage }}-page-{{ $issue.Von }}
{{- else -}}
#page-{{ $issue.Von }}
{{- end -}}
{{- end -}}"
class="inline-flex items-center gap-1 px-2 py-1 bg-slate-100 text-slate-700 rounded-md text-xs font-medium hover:bg-slate-200 transition-colors duration-150"
{{- if and (eq $issue.Nr $model.Number.No) (eq $issue.When.Year
$model.Datum.When.Year)
-}}
aria-current="page"
{{ end }}>
<i class="ri-calendar-line text-xs"></i>
{{- $issue.When.Year }} Nr.
{{ $issue.Nr -}}
</a>
{{ end }}
<!-- Links zu anderen Teilen: -->
{{ if and (not $individualPiece.PieceByIssue.IsContinuation) (gt (len $individualPiece.IssueRefs) 1) }}
<div class="mt-1 pt-1 border-t border-slate-100">
<div class="flex items-center flex-wrap gap-1">
<i class="ri-links-line text-slate-600 text-sm mr-1"></i>
{{ range $index, $issue := $individualPiece.IssueRefs }}
<div
class="inline-flex items-center gap-1 px-2 py-1 bg-slate-100 rounded-md text-xs font-medium hover:bg-slate-200 transition-colors duration-150 {{- if and (eq $issue.Nr $model.Number.No) (eq $issue.When.Year $model.Datum.When.Year) -}}
bg-red-100 text-red-700
{{ end }}">
<span class="text-black text-xs font-bold">{{ add $index 1 }}</span>
<span class="w-px h-3 bg-slate-300 mx-1"></span>
<a
href="/{{- $issue.When -}}/{{- $issue.Nr -}}{{- if $issue.Von -}}
{{- if $issue.Beilage -}}
#beilage-{{ $issue.Beilage }}-page-{{ $issue.Von }}
{{- else -}}
#page-{{ $issue.Von }}
{{- end -}}
{{- end -}}"
class="text-slate-700 no-underline hover:text-slate-900 {{- if and (eq $issue.Nr $model.Number.No) (eq $issue.When.Year $model.Datum.When.Year) -}}
text-red-700 pointer-events-none
{{ end }}"
{{- if and (eq $issue.Nr $model.Number.No) (eq $issue.When.Year
$model.Datum.When.Year)
-}}
aria-current="page"
{{ end }}>
{{- $issueKey := printf "%d-%d" $issue.When.Year $issue.Nr -}}
{{- $issueData := GetIssue $issueKey -}}
{{- if $issueData -}}
{{ $issueData.Datum.When.Day }}.{{ $issueData.Datum.When.Month }}.{{ $issueData.Datum.When.Year }}
{{- else -}}
{{ $issue.When.Year }} Nr.
{{ $issue.Nr }}
{{- end -}}
{{- if $issue.Von }}
S.
{{ $issue.Von }}
{{- end -}}
</a>
</div>
{{ end }}
</div>
</div>
</div>
{{ end }}
</div>
{{ end }}
{{ end }}
</div>
{{ end }}
{{ else }}
<!-- Empty page indicator -->
<div class="inhalts-entry py-1 px-0">
@@ -113,10 +138,16 @@
{{ $allPages := $model.AdditionalPieces.Pages }}
{{ $pageCount := len $allPages }}
{{ $iconType := "first" }}
{{ if eq $pageIndex 0 }}{{ $iconType = "first" }}
{{ else if eq $pageIndex (sub $pageCount 1) }}{{ $iconType = "last" }}
{{ else if eq (mod $pageIndex 2) 1 }}{{ $iconType = "even" }}
{{ else }}{{ $iconType = "odd" }}{{ end }}
{{ if eq $pageIndex 0 }}
{{ $iconType = "first" }}
{{ else if eq $pageIndex (sub $pageCount 1) }}
{{ $iconType = "last" }}
{{ else if eq (mod $pageIndex 2) 1 }}
{{ $iconType = "even" }}
{{ else }}
{{ $iconType = "odd" }}
{{ end }}
<!-- Individual beilage page entry -->
<div
@@ -125,7 +156,13 @@
data-beilage="true">
<div class="flex items-center justify-between gap-2 mb-2">
<div class="flex items-center gap-2">
<span class="icon-container">{{ if $firstItem }}{{ PageIcon $firstItem.PageIcon }}{{ else }}{{ PageIcon $iconType }}{{ end }}</span>
<span class="icon-container"
>{{ if $firstItem }}
{{ PageIcon $firstItem.PageIcon }}
{{ else }}
{{ PageIcon $iconType }}
{{ end }}</span
>
<a
href="#beilage-1-page-{{ $page }}"
class="page-number-inhalts font-bold text-slate-700 bg-amber-50 px-2 py-1 rounded text-sm transition-colors duration-200 hover:bg-amber-100 no-underline"
@@ -147,50 +184,68 @@
<div class="space-y-0">
{{ if $pageItems }}
{{ range $individualPiece := $pageItems }}
<div
class="inhalts-entry py-1 px-0 bg-slate-50 rounded hover:bg-slate-100 transition-colors duration-200 {{ if $individualPiece.PieceByIssue.IsContinuation }}
continuation-entry
{{ end }}"
data-page="{{ $page }}"
{{ if $individualPiece.PieceByIssue.IsContinuation }}
data-is-continuation="true"
{{ end }}
style="{{ if $individualPiece.PieceByIssue.IsContinuation }}
display: none;
{{ end }}">
{{ template "_inhaltsverzeichnis_eintrag" $individualPiece.PieceByIssue }}
<div
class="inhalts-entry py-1 px-0 bg-slate-50 rounded hover:bg-slate-100 transition-colors duration-200 {{ if $individualPiece.PieceByIssue.IsContinuation }}
continuation-entry
{{ end }}"
data-page="{{ $page }}"
{{ if $individualPiece.PieceByIssue.IsContinuation }}
data-is-continuation="true"
{{ end }}
style="{{ if $individualPiece.PieceByIssue.IsContinuation }}
display: none;
{{ end }}">
{{ template "_inhaltsverzeichnis_eintrag" $individualPiece.PieceByIssue }}
<!-- Links zu anderen Teilen: -->
{{ if and (not $individualPiece.PieceByIssue.IsContinuation) (gt (len $individualPiece.IssueRefs) 1) }}
<div class="mt-1 pt-1 border-t border-slate-100">
<div class="flex items-center flex-wrap gap-1">
<i class="ri-links-line text-slate-600 text-sm mr-1"></i>
{{ range $issue := $individualPiece.IssueRefs }}
<a
href="/{{- $issue.When -}}/{{- $issue.Nr -}}{{- if $issue.Von -}}
{{- if $issue.Beilage -}}
#beilage-{{ $issue.Beilage }}-page-{{ $issue.Von }}
{{- else -}}
#page-{{ $issue.Von }}
{{- end -}}
{{- end -}}"
class="inline-flex items-center gap-1 px-2 py-1 bg-slate-100 text-slate-700 rounded-md text-xs font-medium hover:bg-slate-200 transition-colors duration-150"
{{- if and (eq $issue.Nr $model.Number.No) (eq $issue.When.Year
$model.Datum.When.Year)
-}}
aria-current="page"
{{ end }}>
<i class="ri-calendar-line text-xs"></i>
{{- $issue.When.Year }} Nr.
{{ $issue.Nr -}}
</a>
{{ end }}
<!-- Links zu anderen Teilen: -->
{{ if and (not $individualPiece.PieceByIssue.IsContinuation) (gt (len $individualPiece.IssueRefs) 1) }}
<div class="mt-1 pt-1 border-t border-slate-100">
<div class="flex items-center flex-wrap gap-1">
<i class="ri-links-line text-slate-600 text-sm mr-1"></i>
{{ range $index, $issue := $individualPiece.IssueRefs }}
<div
class="inline-flex items-center gap-1 px-2 py-1 bg-slate-100 rounded-md text-xs font-medium hover:bg-slate-200 transition-colors duration-150 {{- if and (eq $issue.Nr $model.Number.No) (eq $issue.When.Year $model.Datum.When.Year) -}}
bg-red-100 text-red-700
{{ end }}">
<span class="text-black text-xs font-bold">{{ add $index 1 }}</span>
<span class="w-px h-3 bg-slate-300 mx-1"></span>
<a
href="/{{- $issue.When -}}/{{- $issue.Nr -}}{{- if $issue.Von -}}
{{- if $issue.Beilage -}}
#beilage-{{ $issue.Beilage }}-page-{{ $issue.Von }}
{{- else -}}
#page-{{ $issue.Von }}
{{- end -}}
{{- end -}}"
class="text-slate-700 no-underline hover:text-slate-900 {{- if and (eq $issue.Nr $model.Number.No) (eq $issue.When.Year $model.Datum.When.Year) -}}
text-red-700 pointer-events-none
{{ end }}"
{{- if and (eq $issue.Nr $model.Number.No) (eq $issue.When.Year
$model.Datum.When.Year)
-}}
aria-current="page"
{{ end }}>
{{- $issueKey := printf "%d-%d" $issue.When.Year $issue.Nr -}}
{{- $issueData := GetIssue $issueKey -}}
{{- if $issueData -}}
{{ $issueData.Datum.When.Day }}.{{ $issueData.Datum.When.Month }}.{{ $issueData.Datum.When.Year }}
{{- else -}}
{{ $issue.When.Year }} Nr.
{{ $issue.Nr }}
{{- end -}}
{{- if $issue.Von }}
S.
{{ $issue.Von }}
{{- end -}}
</a>
</div>
{{ end }}
</div>
</div>
</div>
{{ end }}
</div>
{{ end }}
{{ end }}
</div>
{{ end }}
{{ else }}
<!-- Empty page indicator -->
<div class="inhalts-entry py-1 px-0">

File diff suppressed because it is too large Load Diff

View File

View File

View File

View File

View File

@@ -77,17 +77,958 @@ function setup_templates() {
});
}
// INFO: This is intended to be callled once on website load
// ===========================
// NEWSPAPER LAYOUT FUNCTIONS
// ===========================
// Global variables for state management
window.highlightObserver = window.highlightObserver || null;
window.currentPageContainers = window.currentPageContainers || [];
window.currentActiveIndex = window.currentActiveIndex || 0;
window.pageObserver = window.pageObserver || null;
window.scrollTimeout = window.scrollTimeout || null;
// Page highlighting functionality
function initializePageHighlighting() {
// Clean up existing observer
if (window.highlightObserver) {
window.highlightObserver.disconnect();
window.highlightObserver = null;
}
// Get all page containers
const pageContainers = document.querySelectorAll(".newspaper-page-container");
// Set up intersection observer for active page tracking
window.highlightObserver = new IntersectionObserver(
(entries) => {
checkAndHighlightVisiblePages();
},
{
rootMargin: "-20% 0px -70% 0px",
},
);
// Observe all page containers
pageContainers.forEach((container) => {
window.highlightObserver.observe(container);
});
}
function checkAndHighlightVisiblePages() {
const visiblePageNumbers = [];
const allPageContainers = document.querySelectorAll(".newspaper-page-container");
// Find visible page numbers
allPageContainers.forEach((container) => {
const containerRect = container.getBoundingClientRect();
const viewportHeight = window.innerHeight;
const visibleTop = Math.max(containerRect.top, 0);
const visibleBottom = Math.min(containerRect.bottom, viewportHeight);
const visibleHeight = Math.max(0, visibleBottom - visibleTop);
const containerHeight = containerRect.height;
const visibilityRatio = visibleHeight / containerHeight;
const isVisible = visibilityRatio >= 0.5;
const pageImg = container.querySelector("img[data-page]");
const pageNumber = pageImg ? pageImg.getAttribute("data-page") : "unknown";
if (isVisible && pageImg && pageNumber && !visiblePageNumbers.includes(pageNumber)) {
visiblePageNumbers.push(pageNumber);
}
});
// Show continuations only for visible pages
showContinuationsForVisiblePages(visiblePageNumbers);
// Highlight visible pages
if (visiblePageNumbers.length > 0) {
markCurrentPagesInInhaltsverzeichnis(visiblePageNumbers);
}
}
function showContinuationsForVisiblePages(visiblePageNumbers) {
// Hide all continuation entries by default
document.querySelectorAll(".continuation-entry").forEach((entry) => {
entry.style.display = "none";
});
// Show continuation entries only for visible pages
visiblePageNumbers.forEach((pageNumber) => {
const pageEntry = document.querySelector(`[data-page-container="${pageNumber}"]`);
if (pageEntry) {
const continuationEntries = pageEntry.querySelectorAll(".continuation-entry");
continuationEntries.forEach((entry) => {
entry.style.display = "";
});
}
});
// Update work titles based on highlighted state
updateWorkTitles(visiblePageNumbers);
// Update page entry visibility after showing/hiding continuations
updatePageEntryVisibility();
}
function updateWorkTitles(visiblePageNumbers) {
// Reset all work titles to short form
document.querySelectorAll(".work-title").forEach((titleElement) => {
const shortTitle = titleElement.getAttribute("data-short-title");
if (shortTitle) {
titleElement.textContent = shortTitle;
}
});
// Update work titles to full form for highlighted pages
visiblePageNumbers.forEach((pageNumber) => {
const pageEntry = document.querySelector(`[data-page-container="${pageNumber}"]`);
if (pageEntry) {
const workTitles = pageEntry.querySelectorAll(".work-title");
workTitles.forEach((titleElement) => {
const fullTitle = titleElement.getAttribute("data-full-title");
if (fullTitle && fullTitle !== titleElement.getAttribute("data-short-title")) {
titleElement.textContent = fullTitle;
}
});
}
});
}
function updatePageEntryVisibility() {
// Check each page entry to see if it has any visible content
document.querySelectorAll(".page-entry").forEach((pageEntry) => {
const allEntries = pageEntry.querySelectorAll(".inhalts-entry");
let hasVisibleContent = false;
// Check if any entry is visible
allEntries.forEach((entry) => {
const computedStyle = window.getComputedStyle(entry);
if (computedStyle.display !== "none") {
hasVisibleContent = true;
}
});
// Hide the entire page entry if it has no visible content
if (hasVisibleContent) {
pageEntry.style.display = "";
} else {
pageEntry.style.display = "none";
}
});
}
function markCurrentPageInInhaltsverzeichnis(pageNumber) {
markCurrentPagesInInhaltsverzeichnis([pageNumber]);
}
function markCurrentPagesInInhaltsverzeichnis(pageNumbers) {
console.log("markCurrentPagesInInhaltsverzeichnis called with:", pageNumbers);
// Reset all page container borders to default
document.querySelectorAll("[data-page-container]").forEach((container) => {
if (container.hasAttribute("data-beilage")) {
container.classList.remove("border-red-500");
container.classList.add("border-amber-400");
} else {
container.classList.remove("border-red-500");
container.classList.add("border-slate-300");
}
});
// Reset all page numbers in Inhaltsverzeichnis
document.querySelectorAll(".page-number-inhalts").forEach((pageNum) => {
pageNum.classList.remove("text-red-600", "font-bold");
pageNum.classList.add("text-slate-700", "font-semibold");
pageNum.style.textDecoration = "";
pageNum.style.pointerEvents = "";
// Restore hover effects
if (pageNum.classList.contains("bg-blue-50")) {
pageNum.classList.add("hover:bg-blue-100");
} else if (pageNum.classList.contains("bg-amber-50")) {
pageNum.classList.add("hover:bg-amber-100");
}
// Keep original background colors
if (!pageNum.classList.contains("bg-amber-50") && !pageNum.classList.contains("bg-blue-50")) {
pageNum.classList.add("bg-blue-50");
}
});
// Reset all containers and links in Inhaltsverzeichnis
document.querySelectorAll(".inhalts-entry").forEach((container) => {
container.classList.add("hover:bg-slate-100");
container.style.cursor = "";
});
document.querySelectorAll('.inhalts-entry a[href*="/"]').forEach((link) => {
link.classList.remove("no-underline");
if (link.classList.contains("bg-blue-50")) {
link.classList.add("hover:bg-blue-100");
}
});
// Find and highlight the current page numbers
const highlightedElements = [];
pageNumbers.forEach((pageNumber) => {
// Find the exact page entry for this page number
const pageNumElement = document.querySelector(
`.page-number-inhalts[data-page-number="${pageNumber}"]`,
);
if (pageNumElement) {
// Highlight the page number
pageNumElement.classList.remove(
"text-slate-700",
"hover:bg-blue-100",
"hover:bg-amber-100",
);
pageNumElement.classList.add("text-red-600", "font-bold");
pageNumElement.style.textDecoration = "none";
pageNumElement.style.pointerEvents = "none";
highlightedElements.push(pageNumElement);
// Highlight the page container's left border
const pageContainer = document.querySelector(`[data-page-container="${pageNumber}"]`);
if (pageContainer) {
pageContainer.classList.remove("border-slate-300", "border-amber-400");
pageContainer.classList.add("border-red-500");
}
// Make links in the current page non-clickable and remove hover effects
const parentEntry = pageNumElement.closest(".page-entry");
if (parentEntry) {
// Remove hover effects from the container
const entryContainers = parentEntry.querySelectorAll(".inhalts-entry");
entryContainers.forEach((container) => {
container.classList.remove("hover:bg-slate-100");
container.style.cursor = "default";
});
// Also handle issue reference links
parentEntry.querySelectorAll('a[href*="/"]').forEach((link) => {
if (link.getAttribute("aria-current") === "page") {
link.style.textDecoration = "none";
link.style.pointerEvents = "none";
link.classList.add("no-underline");
link.classList.remove("hover:bg-blue-100");
}
});
}
}
});
// Auto-scroll to first highlighted element if it exists
if (highlightedElements.length > 0) {
scrollToHighlightedPage(highlightedElements[0]);
}
// Also highlight page indicators
document.querySelectorAll(".page-indicator").forEach((indicator) => {
indicator.classList.remove("text-red-600", "font-bold");
indicator.classList.add("text-slate-600", "font-semibold");
// Keep original backgrounds
if (!indicator.classList.contains("bg-amber-50")) {
indicator.classList.add("bg-blue-50");
}
});
// Highlight page indicators for all current pages
pageNumbers.forEach((pageNumber) => {
const pageIndicator = document.querySelector(`.page-indicator[data-page="${pageNumber}"]`);
if (pageIndicator) {
pageIndicator.classList.remove("text-slate-600");
pageIndicator.classList.add("text-red-600", "font-bold");
}
});
}
function scrollToHighlightedPage(element) {
// Check if the element is in a scrollable container
const inhaltsContainer = element.closest(".lg\\:overflow-y-auto");
if (inhaltsContainer) {
// Calculate position
const containerRect = inhaltsContainer.getBoundingClientRect();
const elementRect = element.getBoundingClientRect();
// Check if element is not fully visible
const isAboveContainer = elementRect.top < containerRect.top;
const isBelowContainer = elementRect.bottom > containerRect.bottom;
if (isAboveContainer || isBelowContainer) {
// Scroll to make element visible with some padding
element.scrollIntoView({
behavior: "smooth",
block: "center",
});
}
}
}
// Modal functions
function enlargePage(imgElement, pageNumber, isFromSpread) {
const modal = document.getElementById("pageModal");
const modalImage = document.getElementById("modalImage");
modalImage.src = imgElement.src;
modalImage.alt = imgElement.alt;
modal.classList.remove("hidden");
// Mark current page when enlarged
markCurrentPageInInhaltsverzeichnis(pageNumber);
}
function closeModal() {
const modal = document.getElementById("pageModal");
modal.classList.add("hidden");
}
// Page navigation functions
function initializePageTracking() {
// Clean up existing observer
if (window.pageObserver) {
window.pageObserver.disconnect();
window.pageObserver = null;
}
// Reset state
window.currentPageContainers = Array.from(
document.querySelectorAll(".newspaper-page-container"),
);
window.currentActiveIndex = 0;
updateButtonStates();
// Set up new observer
const existingObserver = document.querySelector(".newspaper-page-container");
if (existingObserver) {
let visibleContainers = new Set();
window.pageObserver = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
const containerIndex = window.currentPageContainers.indexOf(entry.target);
if (containerIndex !== -1) {
if (entry.isIntersecting) {
visibleContainers.add(containerIndex);
} else {
visibleContainers.delete(containerIndex);
}
}
});
// Update currentActiveIndex to the first (topmost) visible container
if (visibleContainers.size > 0) {
const sortedVisible = Array.from(visibleContainers).sort((a, b) => a - b);
const newActiveIndex = sortedVisible[0];
if (newActiveIndex !== window.currentActiveIndex) {
window.currentActiveIndex = newActiveIndex;
updateButtonStates();
}
}
},
{
rootMargin: "-20% 0px -70% 0px",
},
);
window.currentPageContainers.forEach((container) => {
window.pageObserver.observe(container);
});
}
}
function scrollToPreviousPage() {
if (window.currentActiveIndex > 0) {
// Find the first page that's not currently visible
let targetIndex = -1;
// Check which pages are currently visible
const visibleIndexes = [];
window.currentPageContainers.forEach((container, index) => {
const containerRect = container.getBoundingClientRect();
const viewportHeight = window.innerHeight;
const visibleTop = Math.max(containerRect.top, 0);
const visibleBottom = Math.min(containerRect.bottom, viewportHeight);
const visibleHeight = Math.max(0, visibleBottom - visibleTop);
const containerHeight = containerRect.height;
const visibilityRatio = visibleHeight / containerHeight;
if (visibilityRatio >= 0.3) { // Consider visible if 30% or more is showing
visibleIndexes.push(index);
}
});
// Find the first non-visible page before the current visible range
const minVisibleIndex = Math.min(...visibleIndexes);
for (let i = minVisibleIndex - 1; i >= 0; i--) {
if (!visibleIndexes.includes(i)) {
targetIndex = i;
break;
}
}
// If no non-visible page found, go to the page just before the visible range
if (targetIndex === -1 && minVisibleIndex > 0) {
targetIndex = minVisibleIndex - 1;
}
if (targetIndex >= 0) {
window.currentActiveIndex = targetIndex;
window.currentPageContainers[window.currentActiveIndex].scrollIntoView({
behavior: "smooth",
block: "start",
});
// Update button states after a brief delay to let intersection observer catch up
setTimeout(() => {
updateButtonStates();
}, 100);
}
}
}
function scrollToNextPage() {
if (window.currentActiveIndex < window.currentPageContainers.length - 1) {
// Find the first page that's not currently visible
let targetIndex = -1;
// Check which pages are currently visible
const visibleIndexes = [];
window.currentPageContainers.forEach((container, index) => {
const containerRect = container.getBoundingClientRect();
const viewportHeight = window.innerHeight;
const visibleTop = Math.max(containerRect.top, 0);
const visibleBottom = Math.min(containerRect.bottom, viewportHeight);
const visibleHeight = Math.max(0, visibleBottom - visibleTop);
const containerHeight = containerRect.height;
const visibilityRatio = visibleHeight / containerHeight;
if (visibilityRatio >= 0.3) { // Consider visible if 30% or more is showing
visibleIndexes.push(index);
}
});
// Find the first non-visible page after the current visible range
const maxVisibleIndex = Math.max(...visibleIndexes);
for (let i = maxVisibleIndex + 1; i < window.currentPageContainers.length; i++) {
if (!visibleIndexes.includes(i)) {
targetIndex = i;
break;
}
}
// If no non-visible page found, go to the page just after the visible range
if (targetIndex === -1 && maxVisibleIndex < window.currentPageContainers.length - 1) {
targetIndex = maxVisibleIndex + 1;
}
if (targetIndex >= 0 && targetIndex < window.currentPageContainers.length) {
window.currentActiveIndex = targetIndex;
window.currentPageContainers[window.currentActiveIndex].scrollIntoView({
behavior: "smooth",
block: "start",
});
// Update button states after a brief delay to let intersection observer catch up
setTimeout(() => {
updateButtonStates();
}, 100);
}
}
}
function scrollToBeilage() {
// Check if we're currently viewing a Beilage section
const isViewingBeilage = isCurrentlyInBeilageSection();
if (isViewingBeilage) {
// Go back to main issue (first main page)
const firstMainPage = document.querySelector('#newspaper-content .newspaper-page-container');
if (firstMainPage) {
firstMainPage.scrollIntoView({
behavior: "smooth",
block: "start",
});
}
} else {
// Go to first beilage container
const beilageContainer = document.querySelector('[class*="border-t-2 border-amber-200"]');
if (beilageContainer) {
beilageContainer.scrollIntoView({
behavior: "smooth",
block: "start",
});
}
}
}
function isCurrentlyInBeilageSection() {
// Check which pages are currently visible
const visibleIndexes = [];
window.currentPageContainers.forEach((container, index) => {
const containerRect = container.getBoundingClientRect();
const viewportHeight = window.innerHeight;
const visibleTop = Math.max(containerRect.top, 0);
const visibleBottom = Math.min(containerRect.bottom, viewportHeight);
const visibleHeight = Math.max(0, visibleBottom - visibleTop);
const containerHeight = containerRect.height;
const visibilityRatio = visibleHeight / containerHeight;
if (visibilityRatio >= 0.3) { // Consider visible if 30% or more is showing
visibleIndexes.push(index);
}
});
// Check if any visible page is a Beilage page
for (const index of visibleIndexes) {
const container = window.currentPageContainers[index];
if (container && container.id && container.id.includes('beilage-')) {
return true;
}
}
return false;
}
function updateButtonStates() {
const prevBtn = document.getElementById("prevPageBtn");
const nextBtn = document.getElementById("nextPageBtn");
const beilageBtn = document.getElementById("beilageBtn");
if (prevBtn) {
if (window.currentActiveIndex <= 0) {
prevBtn.style.display = "none";
} else {
prevBtn.style.display = "flex";
}
}
if (nextBtn) {
if (window.currentActiveIndex >= window.currentPageContainers.length - 1) {
nextBtn.style.display = "none";
} else {
nextBtn.style.display = "flex";
}
}
// Update Beilage button based on current location
if (beilageBtn) {
const isViewingBeilage = isCurrentlyInBeilageSection();
const icon = beilageBtn.querySelector('i');
if (isViewingBeilage) {
// Show "Go to Main Issue" state - use gray styling
beilageBtn.title = "Zur Hauptausgabe";
beilageBtn.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";
if (icon) {
icon.className = "ri-file-text-line text-lg lg:text-xl";
}
} else {
// Show "Go to Beilage" state - use amber styling
beilageBtn.title = "Zu Beilage";
beilageBtn.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";
if (icon) {
icon.className = "ri-attachment-line text-lg lg:text-xl";
}
}
}
}
// Share and citation functions
function shareCurrentPage() {
const button = document.getElementById("shareLinkBtn");
// Get current page information
let pageInfo = "";
// Try to get the currently visible page number from active containers
if (
window.currentActiveIndex !== undefined &&
window.currentPageContainers &&
window.currentPageContainers[window.currentActiveIndex]
) {
const activeContainer = window.currentPageContainers[window.currentActiveIndex];
const pageElement = activeContainer.querySelector("[data-page]");
if (pageElement) {
const pageNumber = pageElement.getAttribute("data-page");
pageInfo = `#page-${pageNumber}`;
}
}
// Construct the shareable URL
const currentUrl = window.location.origin + window.location.pathname + pageInfo;
// Try to use Web Share API if available (mobile browsers)
if (navigator.share) {
navigator
.share({
title: document.title,
url: currentUrl,
})
.catch((err) => {
// Fallback to clipboard
copyToClipboard(currentUrl, button);
});
} else {
// Fallback: copy to clipboard
copyToClipboard(currentUrl, button);
}
}
function copyToClipboard(text, button) {
if (navigator.clipboard) {
navigator.clipboard
.writeText(text)
.then(() => {
showSimplePopup(button, "Link kopiert!");
})
.catch((err) => {
showSimplePopup(button, "Kopieren fehlgeschlagen");
});
} else {
// Fallback for older browsers
const textarea = document.createElement("textarea");
textarea.value = text;
document.body.appendChild(textarea);
textarea.select();
try {
const successful = document.execCommand("copy");
showSimplePopup(button, successful ? "Link kopiert!" : "Kopieren fehlgeschlagen");
} catch (err) {
showSimplePopup(button, "Kopieren fehlgeschlagen");
} finally {
document.body.removeChild(textarea);
}
}
}
function generateCitation() {
const button = document.getElementById("citationBtn");
// Get current page and issue information
const issueInfo = document.title || "KGPZ";
const currentUrl = window.location.href;
// Basic citation format (can be expanded later)
const currentDate = new Date().toLocaleDateString("de-DE");
const citation = `Königsberger Gelehrte und Politische Zeitung (KGPZ). ${issueInfo}. Digital verfügbar unter: ${currentUrl} (Zugriff: ${currentDate}).`;
// Copy to clipboard
if (navigator.clipboard) {
navigator.clipboard
.writeText(citation)
.then(() => {
showSimplePopup(button, "Zitation kopiert!");
})
.catch((err) => {
showSimplePopup(button, "Kopieren fehlgeschlagen");
});
} else {
// Fallback for older browsers
const textarea = document.createElement("textarea");
textarea.value = citation;
document.body.appendChild(textarea);
textarea.select();
try {
const successful = document.execCommand("copy");
showSimplePopup(button, successful ? "Zitation kopiert!" : "Kopieren fehlgeschlagen");
} catch (err) {
showSimplePopup(button, "Kopieren fehlgeschlagen");
} finally {
document.body.removeChild(textarea);
}
}
}
function showSimplePopup(button, message) {
// Remove any existing popup
const existingPopup = document.querySelector(".simple-popup");
if (existingPopup) {
existingPopup.remove();
}
// Create popup element
const popup = document.createElement("div");
popup.className = "simple-popup";
popup.textContent = message;
// Style the popup
popup.style.cssText = `
position: fixed;
background: #374151;
color: white;
padding: 6px 12px;
border-radius: 6px;
font-size: 13px;
font-weight: 500;
z-index: 1000;
pointer-events: none;
opacity: 0;
transition: opacity 0.2s ease;
white-space: nowrap;
`;
// Position popup next to button
const buttonRect = button.getBoundingClientRect();
const viewportHeight = window.innerHeight;
const viewportWidth = window.innerWidth;
// Calculate position relative to viewport (since we're using fixed positioning)
let left = buttonRect.left - 10;
let top = buttonRect.bottom + 8;
// Ensure popup doesn't go off-screen
const popupWidth = 120; // Estimated popup width
const popupHeight = 32; // Estimated popup height
// Adjust horizontal position if too far right
if (left + popupWidth > viewportWidth) {
left = buttonRect.right - popupWidth + 10;
}
// Adjust vertical position if too far down (show above button instead)
if (top + popupHeight > viewportHeight) {
top = buttonRect.top - popupHeight - 8;
}
popup.style.left = Math.max(5, left) + "px";
popup.style.top = Math.max(5, top) + "px";
// Add to page
document.body.appendChild(popup);
// Fade in
setTimeout(() => {
popup.style.opacity = "1";
}, 10);
// Auto-remove after 2 seconds
setTimeout(() => {
popup.style.opacity = "0";
setTimeout(() => {
if (popup.parentNode) {
popup.remove();
}
}, 200);
}, 2000);
}
// Hash navigation functions
function scrollToPageFromHash() {
const hash = window.location.hash;
let pageNumber = "";
let targetContainer = null;
if (hash.startsWith("#page-")) {
pageNumber = hash.replace("#page-", "");
// Try to find exact page container first
targetContainer = document.getElementById(`page-${pageNumber}`);
// If not found, try to find container that contains this page
if (!targetContainer) {
// Look for double-spread containers that contain this page
const containers = document.querySelectorAll(".newspaper-page-container[data-pages]");
for (const container of containers) {
const pages = container.getAttribute("data-pages");
if (pages && pages.split(",").includes(pageNumber)) {
targetContainer = container;
break;
}
}
}
// If still not found, try beilage containers
if (!targetContainer) {
targetContainer =
document.getElementById(`beilage-1-page-${pageNumber}`) ||
document.getElementById(`beilage-2-page-${pageNumber}`) ||
document.querySelector(`[id*="beilage"][id*="page-${pageNumber}"]`);
}
} else if (hash.startsWith("#beilage-")) {
// Handle beilage-specific hashes like #beilage-1-page-101
const match = hash.match(/#beilage-(\d+)-page-(\d+)/);
if (match) {
const beilageNum = match[1];
pageNumber = match[2];
targetContainer = document.getElementById(`beilage-${beilageNum}-page-${pageNumber}`);
}
}
if (targetContainer && pageNumber) {
setTimeout(() => {
targetContainer.scrollIntoView({
behavior: "smooth",
block: "start",
});
// Highlight the specific page in the table of contents
markCurrentPageInInhaltsverzeichnis(pageNumber);
}, 300);
}
}
// Page-specific utilities
function copyPagePermalink(pageNumber, button, isBeilage = false) {
let pageFragment = "";
if (isBeilage) {
pageFragment = `#beilage-1-page-${pageNumber}`;
} else {
pageFragment = `#page-${pageNumber}`;
}
const currentUrl = window.location.origin + window.location.pathname + pageFragment;
// Copy to clipboard
if (navigator.clipboard) {
navigator.clipboard
.writeText(currentUrl)
.then(() => {
showSimplePopup(button, "Link kopiert!");
})
.catch((err) => {
showSimplePopup(button, "Kopieren fehlgeschlagen");
});
} else {
// Fallback for older browsers
const textarea = document.createElement("textarea");
textarea.value = currentUrl;
document.body.appendChild(textarea);
textarea.select();
try {
const successful = document.execCommand("copy");
showSimplePopup(button, successful ? "Link kopiert!" : "Kopieren fehlgeschlagen");
} catch (err) {
showSimplePopup(button, "Kopieren fehlgeschlagen");
} finally {
document.body.removeChild(textarea);
}
}
}
function generatePageCitation(pageNumber, button) {
// Get current issue information
const issueInfo = document.title || "KGPZ";
const currentUrl = `${window.location.origin}${window.location.pathname}#page-${pageNumber}`;
// Basic citation format for specific page
const currentDate = new Date().toLocaleDateString("de-DE");
const citation = `Königsberger Gelehrte und Politische Zeitung (KGPZ). ${issueInfo}, Seite ${pageNumber}. Digital verfügbar unter: ${currentUrl} (Zugriff: ${currentDate}).`;
// Copy to clipboard
if (navigator.clipboard) {
navigator.clipboard
.writeText(citation)
.then(() => {
showSimplePopup(button, "Zitation kopiert!");
})
.catch((err) => {
showSimplePopup(button, "Kopieren fehlgeschlagen");
});
} else {
// Fallback for older browsers
const textarea = document.createElement("textarea");
textarea.value = citation;
document.body.appendChild(textarea);
textarea.select();
try {
const successful = document.execCommand("copy");
showSimplePopup(button, successful ? "Zitation kopiert!" : "Kopieren fehlgeschlagen");
} catch (err) {
showSimplePopup(button, "Kopieren fehlgeschlagen");
} finally {
document.body.removeChild(textarea);
}
}
}
// Initialize newspaper layout functionality
function initializeNewspaperLayout() {
// Initialize page highlighting
initializePageHighlighting();
// Initialize page tracking
initializePageTracking();
// Set up scroll handler
window.addEventListener("scroll", function () {
clearTimeout(window.scrollTimeout);
window.scrollTimeout = setTimeout(() => {
checkAndHighlightVisiblePages();
updateButtonStates(); // Update button states including Beilage toggle
}, 50);
});
// Initialize hash handling
scrollToPageFromHash();
window.addEventListener("hashchange", scrollToPageFromHash);
// Set up keyboard shortcuts
document.addEventListener("keydown", function (e) {
if (e.key === "Escape") {
closeModal();
}
});
}
// Export functions for global access
window.enlargePage = enlargePage;
window.closeModal = closeModal;
window.scrollToPreviousPage = scrollToPreviousPage;
window.scrollToNextPage = scrollToNextPage;
window.scrollToBeilage = scrollToBeilage;
window.shareCurrentPage = shareCurrentPage;
window.generateCitation = generateCitation;
window.copyPagePermalink = copyPagePermalink;
window.generatePageCitation = generatePageCitation;
// INFO: This is intended to be called once on website load
function setup() {
setup_xslt();
setup_templates();
// Initialize newspaper layout if present
if (document.querySelector(".newspaper-page-container")) {
initializeNewspaperLayout();
}
// Set up HTMX event handlers
htmx.on("htmx:load", function (_) {
// INFO: We can instead use afterSettle; and also clear the map with
// xslt_processors.clear();
setup_xslt();
});
setup_templates();
// HTMX event handling for newspaper layout
document.body.addEventListener("htmx:afterSwap", function (event) {
setTimeout(() => {
if (document.querySelector(".newspaper-page-container")) {
initializeNewspaperLayout();
}
}, 100);
});
document.body.addEventListener("htmx:afterSettle", function (event) {
setTimeout(() => {
if (document.querySelector(".newspaper-page-container")) {
initializeNewspaperLayout();
}
}, 200);
});
document.body.addEventListener("htmx:load", function (event) {
setTimeout(() => {
if (document.querySelector(".newspaper-page-container")) {
initializeNewspaperLayout();
}
}, 100);
});
}
export { setup };

View File

@@ -92,4 +92,72 @@ body::before {
main {
@apply grow shrink-0;
}
/* =============================
NEWSPAPER LAYOUT STYLES
============================= */
/* Essential styles that cannot be represented in Tailwind */
.newspaper-page-container {
margin-bottom: 2rem;
}
/* Page header backdrop blur */
.newspaper-page-container .absolute .page-indicator {
pointer-events: auto;
backdrop-filter: blur(2px);
}
/* Responsive header scaling */
@media (max-width: 768px) {
.newspaper-page-container .absolute {
transform: scale(0.9);
}
}
/* Image size constraints with Tailwind utilities */
.single-page {
display: flex;
justify-content: center;
}
.single-page .newspaper-page-image {
max-width: min(400px, 100%);
width: 100%;
height: auto;
}
/* Larger screens */
@media (min-width: 1280px) {
.single-page .newspaper-page-image {
max-width: min(600px, 100%);
}
}
/* Very wide screens */
@media (min-width: 1536px) {
.single-page .newspaper-page-image {
max-width: min(700px, 100%);
}
}
/* Mobile constraints */
@media (max-width: 640px) {
.single-page .newspaper-page-image {
max-width: 100%;
}
}
/* Navigation button hover styles */
button#prevPageBtn:hover:not([style*="display: none"]),
button#nextPageBtn:hover,
button#beilageBtn:hover {
background-color: rgb(209 213 219) !important; /* gray-300 */
color: rgb(55 65 81) !important; /* gray-700 */
}
button#beilageBtn:hover {
background-color: rgb(254 215 170) !important; /* amber-200 */
color: rgb(146 64 14) !important; /* amber-800 */
}
}