You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

284 lines
12 KiB

  1. // assets/scripts/calendar.js
  2. // Strings aus window.TT.i18n – keine hardcodierten deutschen Texte mehr
  3. function t(key) {
  4. return window.TT?.i18n?.[key] ?? key;
  5. }
  6. class WeekCalendar {
  7. constructor() {
  8. this.nav = document.querySelector('.week-nav');
  9. this.daysContainer = document.querySelector('.week-nav__days');
  10. this.calBtn = document.querySelector('.week-nav__cal');
  11. this.prevBtn = document.querySelector('.week-nav__arrow--prev');
  12. this.nextBtn = document.querySelector('.week-nav__arrow--next');
  13. this.header = document.querySelector('.tt-header');
  14. const raw = this.nav?.dataset.activeDate;
  15. this.activeDate = raw ? new Date(raw + 'T00:00:00') : new Date();
  16. this.today = new Date();
  17. this.today.setHours(0, 0, 0, 0);
  18. this.monthOpen = false;
  19. this.monthDate = new Date(this.activeDate);
  20. this.monthEl = null;
  21. if (!this.nav) return;
  22. this.init();
  23. }
  24. get months() { return t('months') || []; }
  25. get monthsShort() { return t('monthsShort') || []; }
  26. get weekdays() { return t('weekdays') || []; }
  27. get weekdaysShort() { return t('weekdaysShort') || []; }
  28. init() {
  29. this.prevBtn?.addEventListener('click', e => { e.preventDefault(); this.navigateWeek(-1); });
  30. this.nextBtn?.addEventListener('click', e => { e.preventDefault(); this.navigateWeek(1); });
  31. this.calBtn?.addEventListener('click', e => { e.preventDefault(); this.toggleMonth(); });
  32. this.daysContainer?.addEventListener('click', e => {
  33. const dayEl = e.target.closest('.week-nav__day');
  34. if (!dayEl) return;
  35. e.preventDefault();
  36. const dateStr = dayEl.dataset.date;
  37. if (dateStr) this.goToDate(new Date(dateStr + 'T00:00:00'));
  38. });
  39. }
  40. // ── Wochen-Navigation ─────────────────────────────────────────────────────
  41. navigateWeek(direction) {
  42. const slideOut = direction > 0 ? 'slide-out-left' : 'slide-out-right';
  43. const slideIn = direction > 0 ? 'slide-in-right' : 'slide-in-left';
  44. this.daysContainer.classList.add(slideOut);
  45. setTimeout(() => {
  46. const newDate = new Date(this.activeDate);
  47. newDate.setDate(newDate.getDate() + direction * 7);
  48. this.activeDate = newDate;
  49. this.renderWeekDays();
  50. this.updateHeaderMeta();
  51. this.daysContainer.classList.remove(slideOut);
  52. this.daysContainer.classList.add(slideIn);
  53. requestAnimationFrame(() => requestAnimationFrame(() => {
  54. this.daysContainer.classList.remove(slideIn);
  55. }));
  56. window.history.pushState({}, '', `/week/${this.formatDate(this.getMonday(this.activeDate))}`);
  57. window.entryManager?.loadEntriesForDate(this.formatDate(this.activeDate));
  58. }, 180);
  59. }
  60. renderWeekDays() {
  61. const monday = this.getMonday(this.activeDate);
  62. this.daysContainer.innerHTML = '';
  63. for (let i = 0; i < 7; i++) {
  64. const d = new Date(monday);
  65. d.setDate(d.getDate() + i);
  66. const isActive = this.isSameDay(d, this.activeDate);
  67. const isToday = this.isSameDay(d, this.today);
  68. // Führungsnull: padStart(2, '0')
  69. const dayNum = String(d.getDate()).padStart(2, '0');
  70. const monthShort = this.monthsShort[d.getMonth()] ?? '';
  71. const a = document.createElement('a');
  72. a.href = `/week/${this.formatDate(d)}`;
  73. a.className = 'week-nav__day'
  74. + (isActive ? ' week-nav__day--active' : '')
  75. + (isToday ? ' week-nav__day--today' : '');
  76. a.dataset.date = this.formatDate(d);
  77. a.innerHTML = `
  78. <span class="week-nav__day-name">${this.weekdaysShort[i] ?? ''}</span>
  79. <span class="week-nav__day-date">${dayNum}. ${monthShort}</span>
  80. `;
  81. this.daysContainer.appendChild(a);
  82. }
  83. }
  84. goToDate(date) {
  85. this.activeDate = date;
  86. this.renderWeekDays();
  87. this.updateHeaderMeta();
  88. window.history.pushState({}, '', `/week/${this.formatDate(date)}`);
  89. window.entryManager?.loadEntriesForDate(this.formatDate(date));
  90. if (this.monthOpen) this.closeMonth();
  91. }
  92. updateHeaderMeta() {
  93. const dateEl = document.querySelector('.tt-header__date');
  94. const kwEl = document.querySelector('.tt-header__kw');
  95. if (dateEl) {
  96. const d = this.activeDate;
  97. const day = d.getDate();
  98. const month = this.months[d.getMonth()] ?? '';
  99. const tomorrow = new Date(this.today); tomorrow.setDate(this.today.getDate() + 1);
  100. const yesterday = new Date(this.today); yesterday.setDate(this.today.getDate() - 1);
  101. // JS getDay(): 0=So, 1=Mo...6=Sa → weekdays[0]=Montag, also index = getDay()-1, So=6
  102. const jsDay = d.getDay();
  103. const isoIdx = jsDay === 0 ? 6 : jsDay - 1;
  104. const weekday = this.weekdays[isoIdx] ?? '';
  105. let prefix;
  106. if (this.isSameDay(d, this.today)) prefix = t('today');
  107. else if (this.isSameDay(d, tomorrow)) prefix = t('tomorrow');
  108. else if (this.isSameDay(d, yesterday)) prefix = t('yesterday');
  109. else prefix = weekday;
  110. dateEl.textContent = `${prefix}, ${day}. ${month}`;
  111. document.title = `${prefix}, ${day}. ${month}`;
  112. }
  113. if (kwEl) kwEl.textContent = `${t('weekLabel')} ${this.getWeekNumber(this.activeDate)}`;
  114. window._updateWeekToggle?.(this.getWeekNumber(this.activeDate));
  115. }
  116. // ── Monats-Ansicht ────────────────────────────────────────────────────────
  117. toggleMonth() { this.monthOpen ? this.closeMonth() : this.openMonth(); }
  118. openMonth() {
  119. this.monthOpen = true;
  120. this.monthDate = new Date(this.activeDate);
  121. this.monthEl = document.createElement('div');
  122. this.monthEl.className = 'month-calendar month-calendar--hidden';
  123. // Align calendar's right edge with the calendar icon button's right edge
  124. const calRect = this.calBtn.getBoundingClientRect();
  125. const headerRect = this.header.getBoundingClientRect();
  126. this.monthEl.style.right = `${Math.max(0, headerRect.right - calRect.right)}px`;
  127. this.header.appendChild(this.monthEl);
  128. this.renderMonthGrid();
  129. requestAnimationFrame(() => requestAnimationFrame(() => {
  130. this.monthEl.classList.remove('month-calendar--hidden');
  131. this.monthEl.classList.add('month-calendar--visible');
  132. }));
  133. this.calBtn.classList.add('week-nav__cal--active');
  134. }
  135. closeMonth() {
  136. if (!this.monthEl) return;
  137. this.monthEl.classList.remove('month-calendar--visible');
  138. this.monthEl.classList.add('month-calendar--hidden');
  139. setTimeout(() => { this.monthEl?.remove(); this.monthEl = null; }, 280);
  140. this.monthOpen = false;
  141. this.calBtn.classList.remove('week-nav__cal--active');
  142. }
  143. renderMonthGrid() {
  144. if (!this.monthEl) return;
  145. const year = this.monthDate.getFullYear();
  146. const month = this.monthDate.getMonth();
  147. const firstDay = new Date(year, month, 1);
  148. let startDow = firstDay.getDay();
  149. startDow = startDow === 0 ? 6 : startDow - 1;
  150. const daysInMonth = new Date(year, month + 1, 0).getDate();
  151. const daysInPrev = new Date(year, month, 0).getDate();
  152. let html = `
  153. <div class="month-calendar__header">
  154. <button class="month-calendar__arrow month-nav-prev" title="${t('prevMonth')}">
  155. <svg viewBox="0 0 8 14" fill="none"><path d="M7 1L1 7L7 13" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
  156. </button>
  157. <span class="month-calendar__title">${this.months[month] ?? ''} ${year}</span>
  158. <button class="month-calendar__arrow month-nav-next" title="${t('nextMonth')}">
  159. <svg viewBox="0 0 8 14" fill="none"><path d="M1 1L7 7L1 13" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
  160. </button>
  161. <button class="month-calendar__close week-nav__cal" title="${t('monthView')}">
  162. <svg viewBox="0 0 18 18" fill="none">
  163. <rect x="1" y="3" width="16" height="14" rx="2" stroke="currentColor" stroke-width="1.5"/>
  164. <path d="M1 7h16" stroke="currentColor" stroke-width="1.5"/>
  165. <path d="M5 1v4M13 1v4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
  166. <rect x="4" y="10" width="2" height="2" rx="0.5" fill="currentColor"/>
  167. <rect x="8" y="10" width="2" height="2" rx="0.5" fill="currentColor"/>
  168. <rect x="12" y="10" width="2" height="2" rx="0.5" fill="currentColor"/>
  169. </svg>
  170. </button>
  171. </div>
  172. <div class="month-calendar__grid">
  173. <div class="month-calendar__weekdays">
  174. ${this.weekdaysShort.map(d => `<span>${d}</span>`).join('')}
  175. </div>
  176. <div class="month-calendar__days">`;
  177. for (let i = startDow - 1; i >= 0; i--)
  178. html += `<span class="month-day month-day--other">${daysInPrev - i}</span>`;
  179. for (let d = 1; d <= daysInMonth; d++) {
  180. const date = new Date(year, month, d);
  181. const isToday = this.isSameDay(date, this.today);
  182. const isActive= this.isSameDay(date, this.activeDate);
  183. const cls = 'month-day'
  184. + (isToday ? ' month-day--today' : '')
  185. + (isActive ? ' month-day--active' : '');
  186. html += `<span class="${cls}" data-date="${this.formatDate(date)}">${d}</span>`;
  187. }
  188. const totalCells = Math.ceil((startDow + daysInMonth) / 7) * 7;
  189. for (let d = 1; d <= totalCells - startDow - daysInMonth; d++)
  190. html += `<span class="month-day month-day--other">${d}</span>`;
  191. html += `</div></div>`;
  192. this.monthEl.innerHTML = html;
  193. this.monthEl.querySelector('.month-nav-prev')?.addEventListener('click', () => this.navigateMonth(-1));
  194. this.monthEl.querySelector('.month-nav-next')?.addEventListener('click', () => this.navigateMonth(1));
  195. this.monthEl.querySelector('.month-calendar__close')?.addEventListener('click', () => this.closeMonth());
  196. this.monthEl.querySelectorAll('.month-day[data-date]').forEach(el => {
  197. el.addEventListener('click', () => this.goToDate(new Date(el.dataset.date + 'T00:00:00')));
  198. });
  199. }
  200. navigateMonth(direction) {
  201. const grid = this.monthEl?.querySelector('.month-calendar__grid');
  202. if (!grid) return;
  203. const slideOut = direction > 0 ? 'slide-out-left' : 'slide-out-right';
  204. grid.classList.add(slideOut);
  205. setTimeout(() => {
  206. this.monthDate.setMonth(this.monthDate.getMonth() + direction);
  207. this.renderMonthGrid();
  208. }, 160);
  209. }
  210. // ── Hilfsfunktionen ───────────────────────────────────────────────────────
  211. getMonday(date) {
  212. const d = new Date(date);
  213. const day = d.getDay();
  214. d.setDate(d.getDate() - day + (day === 0 ? -6 : 1));
  215. d.setHours(0, 0, 0, 0);
  216. return d;
  217. }
  218. isSameDay(a, b) {
  219. return a.getFullYear() === b.getFullYear()
  220. && a.getMonth() === b.getMonth()
  221. && a.getDate() === b.getDate();
  222. }
  223. formatDate(date) {
  224. const y = date.getFullYear();
  225. const m = String(date.getMonth() + 1).padStart(2, '0');
  226. const d = String(date.getDate()).padStart(2, '0');
  227. return `${y}-${m}-${d}`;
  228. }
  229. getWeekNumber(date) {
  230. const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
  231. d.setUTCDate(d.getUTCDate() + 4 - (d.getUTCDay() || 7));
  232. const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
  233. return Math.ceil((((d - yearStart) / 86400000) + 1) / 7);
  234. }
  235. }
  236. document.addEventListener('DOMContentLoaded', () => { new WeekCalendar(); });