-
![]()
+
-
-
-
-
-
-
-
-
- {{ label.year }}
-
-
-
-
-
@@ -74,6 +30,7 @@ import { ref, computed, watch, onMounted, onUnmounted, nextTick } from 'vue'
import { useEventsStore } from 'stores/events'
import { DEFAULT_TIMELINE_ZOOM, useSettingsStore } from 'stores/settings'
import GlowDot from 'components/GlowDot.vue'
+import { calendarMonthIndex, calendarYear, compareCalendarDates } from 'src/utils/calendarDate'
const emit = defineEmits(['dotSelect', 'viewUpdate'])
const eventsStore = useEventsStore()
@@ -100,8 +57,7 @@ const displayEvents = computed(() => {
const sorted = [...eventsStore.sortedEvents]
if (!showGhost.value) return sorted
const ghost = eventsStore.ghostEvent
- const ghostDate = new Date(ghost.date)
- const insertIdx = sorted.findIndex((e) => new Date(e.date) > ghostDate)
+ const insertIdx = sorted.findIndex((e) => compareCalendarDates(e.date, ghost.date) > 0)
if (insertIdx === -1) {
sorted.push(ghost)
} else {
@@ -122,21 +78,7 @@ function getEventX(index) {
return PADDING.value + index * EVENT_SPACING.value
}
-// Month names
-const MONTHS = ['Jan', 'Feb', 'Mär', 'Apr', 'Mai', 'Jun', 'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Dez']
-
-// One label per event showing its month
-const eventLabels = computed(() => {
- return displayEvents.value.map((event, index) => {
- const d = new Date(event.date)
- return {
- key: `${event.id}-${index}`,
- month: MONTHS[d.getMonth()],
- year: d.getFullYear(),
- fullMonth: `${d.getFullYear()}-${d.getMonth()}`
- }
- })
-})
+const MONTHS_LONG = ['Januar', 'Februar', 'März', 'April', 'Mai', 'Juni', 'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember']
// Year ranges — for each year, the world-space X range of its events
const yearRanges = computed(() => {
@@ -144,11 +86,11 @@ const yearRanges = computed(() => {
const events = displayEvents.value
if (events.length === 0) return ranges
- let currentYear = new Date(events[0].date).getFullYear()
+ let currentYear = calendarYear(events[0].date)
let startIdx = 0
for (let i = 1; i < events.length; i++) {
- const year = new Date(events[i].date).getFullYear()
+ const year = calendarYear(events[i].date)
if (year !== currentYear) {
ranges.push({ year: currentYear, startX: getEventX(startIdx), endX: getEventX(i - 1) })
currentYear = year
@@ -159,57 +101,9 @@ const yearRanges = computed(() => {
return ranges
})
-// All years that exist in the data (for prev/next navigation)
+// All years that exist in the data, ascending (for the year navigation)
const allYears = computed(() => yearRanges.value.map(r => r.year))
-// Sticky year labels — positioned relative to viewport, clamped to edges
-const YEAR_MARGIN = 24
-const stickyYearLabels = computed(() => {
- const sl = scrollLeft.value
- const vw = viewportWidth.value
- const viewLeft = sl
- const viewRight = sl + vw
- const years = allYears.value
-
- // Find years whose event range overlaps the viewport
- const visible = yearRanges.value.filter(r => r.endX >= viewLeft && r.startX <= viewRight)
- if (visible.length === 0) return []
-
- function makeLabel(year, left) {
- const idx = years.indexOf(year)
- return { year, left, hasPrev: idx > 0, hasNext: idx < years.length - 1 }
- }
-
- if (visible.length === 1) {
- const r = visible[0]
- const visStart = Math.max(r.startX, viewLeft)
- const visEnd = Math.min(r.endX, viewRight)
- const center = (visStart + visEnd) / 2 - sl
- const clamped = Math.max(YEAR_MARGIN, Math.min(vw - YEAR_MARGIN, center))
- return [makeLabel(r.year, clamped)]
- }
-
- // Multiple years: first pins left, last pins right, middles float naturally
- const result = []
- for (let i = 0; i < visible.length; i++) {
- const r = visible[i]
- const visStart = Math.max(r.startX, viewLeft)
- const visEnd = Math.min(r.endX, viewRight)
- const center = (visStart + visEnd) / 2 - sl
-
- let pos
- if (i === 0) {
- pos = Math.max(YEAR_MARGIN, Math.min(vw / 2 - 30, center))
- } else if (i === visible.length - 1) {
- pos = Math.min(vw - YEAR_MARGIN, Math.max(vw / 2 + 30, center))
- } else {
- pos = Math.max(YEAR_MARGIN + 60, Math.min(vw - YEAR_MARGIN - 60, center))
- }
- result.push(makeLabel(r.year, pos))
- }
- return result
-})
-
// Virtualization: only render events near the viewport
const VIS_BUFFER = 2
const VIEW_EMIT_BUFFER = 3
@@ -238,25 +132,27 @@ const visibleEvents = computed(() => {
}))
})
-const visibleLabels = computed(() => {
- const { start, end } = visibleRange.value
- if (end < start) return []
- return eventLabels.value.slice(start, end + 1).map((label, i) => ({
- label,
- globalIndex: start + i
- }))
-})
-
-// Active label — closest event to center of viewport (O(1) via index)
-const activeLabel = computed(() => {
+// The event closest to the viewport centre (O(1) via index)
+const activeIndex = computed(() => {
const total = displayEvents.value.length
- if (total === 0) return null
+ if (total === 0) return -1
const centerX = scrollLeft.value + viewportWidth.value / 2
const spacing = EVENT_SPACING.value
- if (spacing <= 0) return null
+ if (spacing <= 0) return 0
const index = Math.round((centerX - PADDING.value) / spacing)
- const clamped = Math.max(0, Math.min(total - 1, index))
- return eventLabels.value[clamped]?.key ?? null
+ return Math.max(0, Math.min(total - 1, index))
+})
+
+// Month and year of the centred event, read from the calendar date so the
+// navigation never drifts into the neighbouring month in western zones.
+const activePosition = computed(() => {
+ const event = displayEvents.value[activeIndex.value]
+ if (!event) return null
+ const month = calendarMonthIndex(event.date)
+ return {
+ year: calendarYear(event.date),
+ month: month === null ? '' : MONTHS_LONG[month]
+ }
})
function onScroll() {
@@ -267,15 +163,6 @@ function onScroll() {
}
}
-function scrollToIndex(index) {
- if (!timelineRef.value) return
- const x = getEventX(index)
- timelineRef.value.scrollTo({
- left: x - viewportWidth.value / 2,
- behavior: 'smooth'
- })
-}
-
function scrollToYearCenter(year) {
if (!timelineRef.value) return
// Find exact year, or nearest in the requested direction
@@ -632,7 +519,19 @@ function zoomTo(value) {
if (newZoom !== zoomLevel.value) applyZoom(newZoom)
}
-defineExpose({ timelineRef, zoomIn, zoomOut, zoomTo, zoomLevel, MIN_ZOOM, MAX_ZOOM, forceReflow })
+defineExpose({
+ timelineRef,
+ zoomIn,
+ zoomOut,
+ zoomTo,
+ zoomLevel,
+ MIN_ZOOM,
+ MAX_ZOOM,
+ forceReflow,
+ activePosition,
+ years: allYears,
+ scrollToYearCenter
+})
diff --git a/frontend/src/components/UserMenu.vue b/frontend/src/components/UserMenu.vue
index 5480c19..7da01b5 100644
--- a/frontend/src/components/UserMenu.vue
+++ b/frontend/src/components/UserMenu.vue
@@ -14,70 +14,28 @@
-
+
-
-
-
-
-
@@ -96,7 +54,7 @@