// assets/scripts/calendar.js // Strings aus window.TT.i18n – keine hardcodierten deutschen Texte mehr function t(key) { return window.TT?.i18n?.[key] ?? key; } class WeekCalendar { constructor() { this.nav = document.querySelector('.week-nav'); this.daysContainer = document.querySelector('.week-nav__days'); this.calBtn = document.querySelector('.week-nav__cal'); this.prevBtn = document.querySelector('.week-nav__arrow--prev'); this.nextBtn = document.querySelector('.week-nav__arrow--next'); this.header = document.querySelector('.tt-header'); const raw = this.nav?.dataset.activeDate; this.activeDate = raw ? new Date(raw + 'T00:00:00') : new Date(); this.today = new Date(); this.today.setHours(0, 0, 0, 0); this.monthOpen = false; this.monthDate = new Date(this.activeDate); this.monthEl = null; if (!this.nav) return; this.init(); } get months() { return t('months') || []; } get monthsShort() { return t('monthsShort') || []; } get weekdays() { return t('weekdays') || []; } get weekdaysShort() { return t('weekdaysShort') || []; } init() { this.prevBtn?.addEventListener('click', e => { e.preventDefault(); this.navigateWeek(-1); }); this.nextBtn?.addEventListener('click', e => { e.preventDefault(); this.navigateWeek(1); }); this.calBtn?.addEventListener('click', e => { e.preventDefault(); this.toggleMonth(); }); this.daysContainer?.addEventListener('click', e => { const dayEl = e.target.closest('.week-nav__day'); if (!dayEl) return; e.preventDefault(); const dateStr = dayEl.dataset.date; if (dateStr) this.goToDate(new Date(dateStr + 'T00:00:00')); }); } // ── Wochen-Navigation ───────────────────────────────────────────────────── navigateWeek(direction) { const slideOut = direction > 0 ? 'slide-out-left' : 'slide-out-right'; const slideIn = direction > 0 ? 'slide-in-right' : 'slide-in-left'; this.daysContainer.classList.add(slideOut); setTimeout(() => { const newDate = new Date(this.activeDate); newDate.setDate(newDate.getDate() + direction * 7); this.activeDate = newDate; this.renderWeekDays(); this.updateHeaderMeta(); this.daysContainer.classList.remove(slideOut); this.daysContainer.classList.add(slideIn); requestAnimationFrame(() => requestAnimationFrame(() => { this.daysContainer.classList.remove(slideIn); })); window.history.pushState({}, '', `/week/${this.formatDate(this.getMonday(this.activeDate))}`); window.entryManager?.loadEntriesForDate(this.formatDate(this.activeDate)); }, 180); } renderWeekDays() { const monday = this.getMonday(this.activeDate); this.daysContainer.innerHTML = ''; for (let i = 0; i < 7; i++) { const d = new Date(monday); d.setDate(d.getDate() + i); const isActive = this.isSameDay(d, this.activeDate); const isToday = this.isSameDay(d, this.today); // Führungsnull: padStart(2, '0') const dayNum = String(d.getDate()).padStart(2, '0'); const monthShort = this.monthsShort[d.getMonth()] ?? ''; const a = document.createElement('a'); a.href = `/week/${this.formatDate(d)}`; a.className = 'week-nav__day' + (isActive ? ' week-nav__day--active' : '') + (isToday ? ' week-nav__day--today' : ''); a.dataset.date = this.formatDate(d); a.innerHTML = ` ${this.weekdaysShort[i] ?? ''} ${dayNum}. ${monthShort} `; this.daysContainer.appendChild(a); } } goToDate(date) { this.activeDate = date; this.renderWeekDays(); this.updateHeaderMeta(); window.history.pushState({}, '', `/week/${this.formatDate(date)}`); window.entryManager?.loadEntriesForDate(this.formatDate(date)); if (this.monthOpen) this.closeMonth(); } updateHeaderMeta() { const dateEl = document.querySelector('.tt-header__date'); const kwEl = document.querySelector('.tt-header__kw'); if (dateEl) { const d = this.activeDate; const day = d.getDate(); const month = this.months[d.getMonth()] ?? ''; const tomorrow = new Date(this.today); tomorrow.setDate(this.today.getDate() + 1); const yesterday = new Date(this.today); yesterday.setDate(this.today.getDate() - 1); // JS getDay(): 0=So, 1=Mo...6=Sa → weekdays[0]=Montag, also index = getDay()-1, So=6 const jsDay = d.getDay(); const isoIdx = jsDay === 0 ? 6 : jsDay - 1; const weekday = this.weekdays[isoIdx] ?? ''; let prefix; if (this.isSameDay(d, this.today)) prefix = t('today'); else if (this.isSameDay(d, tomorrow)) prefix = t('tomorrow'); else if (this.isSameDay(d, yesterday)) prefix = t('yesterday'); else prefix = weekday; dateEl.textContent = `${prefix}, ${day}. ${month}`; document.title = `${prefix}, ${day}. ${month}`; } if (kwEl) kwEl.textContent = `${t('weekLabel')} ${this.getWeekNumber(this.activeDate)}`; } // ── Monats-Ansicht ──────────────────────────────────────────────────────── toggleMonth() { this.monthOpen ? this.closeMonth() : this.openMonth(); } openMonth() { this.monthOpen = true; this.monthDate = new Date(this.activeDate); this.monthEl = document.createElement('div'); this.monthEl.className = 'month-calendar month-calendar--hidden'; this.header.appendChild(this.monthEl); this.renderMonthGrid(); requestAnimationFrame(() => requestAnimationFrame(() => { this.monthEl.classList.remove('month-calendar--hidden'); this.monthEl.classList.add('month-calendar--visible'); })); this.calBtn.classList.add('week-nav__cal--active'); } closeMonth() { if (!this.monthEl) return; this.monthEl.classList.remove('month-calendar--visible'); this.monthEl.classList.add('month-calendar--hidden'); setTimeout(() => { this.monthEl?.remove(); this.monthEl = null; }, 280); this.monthOpen = false; this.calBtn.classList.remove('week-nav__cal--active'); } renderMonthGrid() { if (!this.monthEl) return; const year = this.monthDate.getFullYear(); const month = this.monthDate.getMonth(); const firstDay = new Date(year, month, 1); let startDow = firstDay.getDay(); startDow = startDow === 0 ? 6 : startDow - 1; const daysInMonth = new Date(year, month + 1, 0).getDate(); const daysInPrev = new Date(year, month, 0).getDate(); let html = `
${this.months[month] ?? ''} ${year}
${this.weekdaysShort.map(d => `${d}`).join('')}
`; for (let i = startDow - 1; i >= 0; i--) html += `${daysInPrev - i}`; for (let d = 1; d <= daysInMonth; d++) { const date = new Date(year, month, d); const isToday = this.isSameDay(date, this.today); const isActive= this.isSameDay(date, this.activeDate); const cls = 'month-day' + (isToday ? ' month-day--today' : '') + (isActive ? ' month-day--active' : ''); html += `${d}`; } const totalCells = Math.ceil((startDow + daysInMonth) / 7) * 7; for (let d = 1; d <= totalCells - startDow - daysInMonth; d++) html += `${d}`; html += `
`; this.monthEl.innerHTML = html; this.monthEl.querySelector('.month-nav-prev')?.addEventListener('click', () => this.navigateMonth(-1)); this.monthEl.querySelector('.month-nav-next')?.addEventListener('click', () => this.navigateMonth(1)); this.monthEl.querySelector('.month-calendar__close')?.addEventListener('click', () => this.closeMonth()); this.monthEl.querySelectorAll('.month-day[data-date]').forEach(el => { el.addEventListener('click', () => this.goToDate(new Date(el.dataset.date + 'T00:00:00'))); }); } navigateMonth(direction) { const grid = this.monthEl?.querySelector('.month-calendar__grid'); if (!grid) return; const slideOut = direction > 0 ? 'slide-out-left' : 'slide-out-right'; grid.classList.add(slideOut); setTimeout(() => { this.monthDate.setMonth(this.monthDate.getMonth() + direction); this.renderMonthGrid(); }, 160); } // ── Hilfsfunktionen ─────────────────────────────────────────────────────── getMonday(date) { const d = new Date(date); const day = d.getDay(); d.setDate(d.getDate() - day + (day === 0 ? -6 : 1)); d.setHours(0, 0, 0, 0); return d; } isSameDay(a, b) { return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate(); } formatDate(date) { const y = date.getFullYear(); const m = String(date.getMonth() + 1).padStart(2, '0'); const d = String(date.getDate()).padStart(2, '0'); return `${y}-${m}-${d}`; } getWeekNumber(date) { const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate())); d.setUTCDate(d.getUTCDate() + 4 - (d.getUTCDay() || 7)); const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1)); return Math.ceil((((d - yearStart) / 86400000) + 1) / 7); } } document.addEventListener('DOMContentLoaded', () => { new WeekCalendar(); });