mirror of
https://github.com/Theodor-Springmann-Stiftung/kgpz_web.git
synced 2025-10-30 17:45:30 +00:00
Start places overhaul
This commit is contained in:
74
views/routes/ort/components/README.md
Normal file
74
views/routes/ort/components/README.md
Normal file
@@ -0,0 +1,74 @@
|
||||
# Orte Components Documentation
|
||||
|
||||
## Overview
|
||||
The Orte (Places) section has been updated to use an expandable list view instead of a card grid. This provides better usability on mobile devices and allows for lazy-loading of place details.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Templates
|
||||
- `_place_expandable.gohtml` - Template for individual expandable place entries (currently unused, kept for reference)
|
||||
- `_place_details_fragment.gohtml` - HTMX fragment for place details (header + pieces list)
|
||||
|
||||
### JavaScript Components
|
||||
- `ExpandablePlacesList` - Main web component for the expandable places list
|
||||
- No shadow DOM - full Tailwind CSS support
|
||||
- Property-based data passing
|
||||
- HTMX integration for lazy-loading details
|
||||
|
||||
### Backend
|
||||
- `GetPlaceDetails()` - New controller handler for HTMX requests
|
||||
- Route: `/ort/{place}/details`
|
||||
- Returns place details fragment
|
||||
|
||||
## Features
|
||||
|
||||
### Expandable Interaction
|
||||
- Click to expand/collapse place entries
|
||||
- Only one place can be expanded at a time
|
||||
- Smooth CSS transitions for expand/collapse
|
||||
- Keyboard navigation support (Enter/Space)
|
||||
|
||||
### Data Loading
|
||||
- Initial places list loaded from server-side template
|
||||
- Place details loaded on-demand via HTMX
|
||||
- Loading states and error handling
|
||||
- Caching of loaded content
|
||||
|
||||
### Accessibility
|
||||
- Proper ARIA attributes (`aria-expanded`, `aria-hidden`, `aria-controls`)
|
||||
- Keyboard navigation support
|
||||
- Screen reader friendly labels
|
||||
- Focus management
|
||||
|
||||
### Preserved Functionality
|
||||
- `/ort/{id}` permalink URLs still work for direct access
|
||||
- Search filtering via existing `GenericFilter` component
|
||||
- External links (Wikipedia, Geonames, OpenStreetMap) in expanded view
|
||||
- Citation links to specific newspaper issues
|
||||
|
||||
## Usage
|
||||
|
||||
The component is automatically initialized on page load with data from the Go template:
|
||||
|
||||
```html
|
||||
<expandable-places-list id="places-list"></expandable-places-list>
|
||||
<script>
|
||||
const placesList = document.getElementById('places-list');
|
||||
placesList.places = placesData; // Array of place objects
|
||||
</script>
|
||||
```
|
||||
|
||||
## Data Structure
|
||||
|
||||
Each place object contains:
|
||||
- `ID`: Place identifier
|
||||
- `Names`: Array of place names
|
||||
- `Geo`: Geonames URL (optional)
|
||||
- `PieceCount`: Number of associated pieces
|
||||
|
||||
## Technical Notes
|
||||
|
||||
- Uses CSS `max-height` transitions for smooth expand/collapse
|
||||
- HTMX events handled for loading states
|
||||
- Event delegation for dynamically created content
|
||||
- Compatible with existing search filtering system
|
||||
179
views/routes/ort/components/_place_details_fragment.gohtml
Normal file
179
views/routes/ort/components/_place_details_fragment.gohtml
Normal file
@@ -0,0 +1,179 @@
|
||||
{{- /*
|
||||
Place Details Fragment
|
||||
Used for HTMX requests to load place details into expandable content
|
||||
Contains place header information and associated pieces
|
||||
*/ -}}
|
||||
|
||||
{{- /* Place Header Information */ -}}
|
||||
<div class="mb-6">
|
||||
{{ $geonames := GetGeonames .place.Place.Geo }}
|
||||
|
||||
{{- /* Name and external links */ -}}
|
||||
<div class="flex items-start justify-between gap-4 mb-4">
|
||||
<div class="flex-1">
|
||||
<h2 class="text-xl font-bold text-slate-800 mb-2">
|
||||
{{ if .place.Place.Names }}
|
||||
{{ index .place.Place.Names 0 }}
|
||||
{{ else }}
|
||||
{{ .place.Place.ID }}
|
||||
{{ end }}
|
||||
</h2>
|
||||
|
||||
{{- /* Geographic Information from Geonames */ -}}
|
||||
{{ if ne $geonames nil }}
|
||||
<div class="text-sm text-slate-700 mb-2">
|
||||
{{- /* Modern Country Info (only if not Germany) */ -}}
|
||||
{{ $mainPlaceName := "" }}
|
||||
{{ if .place.Place.Names }}
|
||||
{{ $mainPlaceName = index .place.Place.Names 0 }}
|
||||
{{ end }}
|
||||
{{ $fullInfo := GetFullPlaceInfo .place.Place.Geo $mainPlaceName }}
|
||||
{{ if ne $fullInfo "" }}
|
||||
<div class="mb-1">{{ $fullInfo }}</div>
|
||||
{{ end }}
|
||||
|
||||
{{- /* Coordinates */ -}}
|
||||
<div class="text-slate-600 text-sm space-y-1">
|
||||
{{ if and (ne $geonames.Lat "") (ne $geonames.Lng "") }}
|
||||
<div>
|
||||
<i class="ri-map-pin-line mr-1"></i><a href="https://www.openstreetmap.org/?mlat={{ $geonames.Lat }}&mlon={{ $geonames.Lng }}&zoom=12" target="_blank" rel="noopener noreferrer" class="text-blue-600 hover:text-blue-700 underline">{{ $geonames.Lat }}, {{ $geonames.Lng }}</a>
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
{{ else }}
|
||||
{{- /* Fallback when no Geonames data */ -}}
|
||||
{{ if .place.Place.Geo }}
|
||||
<p class="text-slate-600 mb-2 text-sm">
|
||||
<i class="ri-map-pin-line mr-1"></i>
|
||||
<a href="{{ .place.Place.Geo }}" target="_blank" rel="noopener noreferrer" class="text-blue-600 hover:text-blue-700 underline">
|
||||
Geonames
|
||||
</a>
|
||||
</p>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
</div>
|
||||
|
||||
{{- /* External link symbols on the right */ -}}
|
||||
<div class="flex gap-2 flex-shrink-0 items-center">
|
||||
{{ if ne $geonames nil }}
|
||||
{{- /* Wikipedia link if available */ -}}
|
||||
{{ if ne $geonames.WikipediaURL "" }}
|
||||
<a href="https://{{ $geonames.WikipediaURL }}" target="_blank" class="hover:opacity-80 transition-opacity" title="Wikipedia">
|
||||
<img src="/assets/wikipedia.png" alt="Wikipedia" class="w-5 h-5">
|
||||
</a>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
{{- /* Geonames link */ -}}
|
||||
{{ if .place.Place.Geo }}
|
||||
<a href="{{ .place.Place.Geo }}" target="_blank" class="hover:opacity-80 transition-opacity no-underline" title="Geonames">
|
||||
<i class="ri-global-line text-lg text-blue-600"></i>
|
||||
</a>
|
||||
{{ end }}
|
||||
|
||||
{{- /* OpenStreetMap link */ -}}
|
||||
{{ if and (ne $geonames nil) (ne $geonames.Lat "") (ne $geonames.Lng "") }}
|
||||
<a href="https://www.openstreetmap.org/?mlat={{ $geonames.Lat }}&mlon={{ $geonames.Lng }}&zoom=12" target="_blank" class="hover:opacity-80 transition-opacity" title="OpenStreetMap">
|
||||
<i class="ri-map-2-line text-lg text-green-600"></i>
|
||||
</a>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{- /* Associated Pieces */ -}}
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold text-slate-800 mb-3">
|
||||
<i class="ri-newspaper-line mr-2"></i>Verlinkte Beiträge ({{ len .place.Pieces }})
|
||||
</h3>
|
||||
|
||||
{{ if .place.Pieces }}
|
||||
{{- /* Group pieces by year */ -}}
|
||||
{{- $piecesByYear := dict -}}
|
||||
{{- range $_, $p := .place.Pieces -}}
|
||||
{{- range $issueRef := $p.IssueRefs -}}
|
||||
{{- $year := printf "%d" $issueRef.When.Year -}}
|
||||
{{- $existing := index $piecesByYear $year -}}
|
||||
{{- if $existing -}}
|
||||
{{- $piecesByYear = merge $piecesByYear (dict $year (append $existing $p)) -}}
|
||||
{{- else -}}
|
||||
{{- $piecesByYear = merge $piecesByYear (dict $year (slice $p)) -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- /* Get sorted years */ -}}
|
||||
{{- $sortedYears := slice -}}
|
||||
{{- range $year, $pieces := $piecesByYear -}}
|
||||
{{- $sortedYears = append $sortedYears $year -}}
|
||||
{{- end -}}
|
||||
{{- $sortedYears = sortStrings $sortedYears -}}
|
||||
|
||||
<div class="space-y-4 max-w-[85ch]">
|
||||
{{- range $year := $sortedYears -}}
|
||||
{{- $yearPieces := index $piecesByYear $year -}}
|
||||
|
||||
{{- /* Year Header */ -}}
|
||||
<div>
|
||||
<h4 class="text-base font-bold font-serif text-slate-800 mb-2">{{ $year }}</h4>
|
||||
|
||||
<div class="space-y-1 text-sm">
|
||||
{{- /* Group pieces by title within each year */ -}}
|
||||
{{- $groupedPieces := dict -}}
|
||||
{{- range $_, $p := $yearPieces -}}
|
||||
{{- $groupKey := "" -}}
|
||||
{{- if $p.Title -}}
|
||||
{{- $groupKey = index $p.Title 0 -}}
|
||||
{{- else if $p.Incipit -}}
|
||||
{{- $groupKey = index $p.Incipit 0 -}}
|
||||
{{- else -}}
|
||||
{{- $groupKey = printf "untitled-%s" $p.ID -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- $existing := index $groupedPieces $groupKey -}}
|
||||
{{- if $existing -}}
|
||||
{{- $groupedPieces = merge $groupedPieces (dict $groupKey (append $existing $p)) -}}
|
||||
{{- else -}}
|
||||
{{- $groupedPieces = merge $groupedPieces (dict $groupKey (slice $p)) -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- range $groupKey, $groupedItems := $groupedPieces -}}
|
||||
<div class="pb-1 leading-relaxed">
|
||||
{{- /* Use first piece for display text with colon format for places */ -}}
|
||||
{{ template "_unified_piece_entry" (dict "Piece" (index $groupedItems 0) "CurrentActorID" "" "DisplayMode" "place" "ShowPlaceTags" false "UseColonFormat" true "ShowContinuation" false) }}
|
||||
|
||||
{{- /* Show all citations from all pieces in this group inline with commas */ -}}
|
||||
{{ " " }}{{- range $groupIndex, $groupItem := $groupedItems -}}
|
||||
{{- range $issueIndex, $issue := $groupItem.IssueRefs -}}
|
||||
{{- /* Only show citations for the current year */ -}}
|
||||
{{- if eq (printf "%d" $issue.When.Year) $year -}}
|
||||
{{- if or (gt $groupIndex 0) (gt $issueIndex 0) }}, {{ end -}}
|
||||
<span class="text-blue-600 hover:text-blue-700 underline decoration-dotted hover:decoration-solid [&>a]:text-blue-600 [&>a:hover]:text-blue-700">{{- template "_citation" $issue -}}</span>
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- /* Add "Ganzer Beitrag" link if piece spans multiple issues */ -}}
|
||||
{{- $firstGroupItem := index $groupedItems 0 -}}
|
||||
{{- if gt (len $firstGroupItem.IssueRefs) 1 -}}
|
||||
{{ " " }}<span class="inline-flex items-center gap-1 px-2 py-1 bg-blue-50
|
||||
hover:bg-blue-100 text-blue-700 hover:text-blue-800 border border-blue-200
|
||||
hover:border-blue-300 rounded text-xs font-medium transition-colors duration-200">
|
||||
<i class="ri-file-copy-2-line text-xs"></i>
|
||||
<a href="{{ GetPieceURL $firstGroupItem.ID }}">
|
||||
Ganzer Beitrag
|
||||
</a>
|
||||
</span>
|
||||
{{- end }}
|
||||
</div>
|
||||
{{- end -}}
|
||||
</div>
|
||||
</div>
|
||||
{{- end -}}
|
||||
</div>
|
||||
{{ else }}
|
||||
<p class="text-slate-500 italic text-sm">Keine verlinkten Beiträge für diesen Ort gefunden.</p>
|
||||
{{ end }}
|
||||
</div>
|
||||
79
views/routes/ort/components/_place_expandable.gohtml
Normal file
79
views/routes/ort/components/_place_expandable.gohtml
Normal file
@@ -0,0 +1,79 @@
|
||||
{{- /*
|
||||
Expandable Place Entry Template
|
||||
This is used as a JavaScript template string in the ExpandablePlacesList component
|
||||
Variables: place, geonames, mainPlaceName, modernName, fullInfo, pieceCount
|
||||
*/ -}}
|
||||
|
||||
<div class="border border-slate-200 rounded-lg hover:border-slate-300 transition-colors duration-200"
|
||||
data-place-name="${mainPlaceName}"
|
||||
data-modern-name="${modernName}"
|
||||
data-place-id="${place.ID}">
|
||||
|
||||
{{- /* Collapsed State - Always Visible */ -}}
|
||||
<div class="p-4 cursor-pointer select-none"
|
||||
data-toggle="collapse"
|
||||
data-target="place-${place.ID}-content"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
aria-expanded="false"
|
||||
aria-controls="place-${place.ID}-content">
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center gap-3">
|
||||
{{- /* Expand/Collapse Icon */ -}}
|
||||
<div class="transition-transform duration-200" data-expand-icon>
|
||||
<svg class="w-5 h-5 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
{{- /* Place Name as clickable link */ -}}
|
||||
<div>
|
||||
<a href="/ort/${place.ID}"
|
||||
class="text-lg font-medium text-slate-800 hover:text-slate-900 hover:underline"
|
||||
onclick="event.stopPropagation()">
|
||||
${mainPlaceName}
|
||||
</a>
|
||||
|
||||
{{- /* Geographic info if available */ -}}
|
||||
\${fullInfo ? \`
|
||||
<p class="text-sm text-slate-600 mt-1">
|
||||
<i class="ri-map-pin-line mr-1"></i>\${fullInfo}
|
||||
</p>
|
||||
\` : ''}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{- /* Piece count badge */ -}}
|
||||
<div class="flex items-center gap-2 text-sm text-slate-500">
|
||||
<span>\${pieceCount} Beiträge</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{- /* Expandable Content Area */ -}}
|
||||
<div id="place-${place.ID}-content"
|
||||
class="overflow-hidden transition-all duration-300 ease-in-out max-h-0"
|
||||
data-content-area
|
||||
aria-hidden="true">
|
||||
|
||||
<div class="border-t border-slate-200 p-4 bg-slate-50">
|
||||
{{- /* Loading state */ -}}
|
||||
<div class="flex items-center justify-center py-8" data-loading-state>
|
||||
<div class="flex items-center gap-2 text-slate-500">
|
||||
<div class="animate-spin">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
|
||||
</svg>
|
||||
</div>
|
||||
<span>Lade Beiträge...</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{- /* Content will be loaded here via HTMX */ -}}
|
||||
<div data-place-details></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
Reference in New Issue
Block a user