Start places overhaul

This commit is contained in:
Simon Martens
2025-09-28 03:53:03 +02:00
parent 2a3d2c2323
commit adc45f2212
13 changed files with 683 additions and 42 deletions

View 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

View 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>

View 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>