Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.
 
 
 
 
 

277 lignes
11 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. }
  115. // ── Monats-Ansicht ────────────────────────────────────────────────────────
  116. toggleMonth() { this.monthOpen ? this.closeMonth() : this.openMonth(); }
  117. openMonth() {
  118. this.monthOpen = true;
  119. this.monthDate = new Date(this.activeDate);
  120. this.monthEl = document.createElement('div');
  121. this.monthEl.className = 'month-calendar month-calendar--hidden';
  122. this.header.appendChild(this.monthEl);
  123. this.renderMonthGrid();
  124. requestAnimationFrame(() => requestAnimationFrame(() => {
  125. this.monthEl.classList.remove('month-calendar--hidden');
  126. this.monthEl.classList.add('month-calendar--visible');
  127. }));
  128. this.calBtn.classList.add('week-nav__cal--active');
  129. }
  130. closeMonth() {
  131. if (!this.monthEl) return;
  132. this.monthEl.classList.remove('month-calendar--visible');
  133. this.monthEl.classList.add('month-calendar--hidden');
  134. setTimeout(() => { this.monthEl?.remove(); this.monthEl = null; }, 280);
  135. this.monthOpen = false;
  136. this.calBtn.classList.remove('week-nav__cal--active');
  137. }
  138. renderMonthGrid() {
  139. if (!this.monthEl) return;
  140. const year = this.monthDate.getFullYear();
  141. const month = this.monthDate.getMonth();
  142. const firstDay = new Date(year, month, 1);
  143. let startDow = firstDay.getDay();
  144. startDow = startDow === 0 ? 6 : startDow - 1;
  145. const daysInMonth = new Date(year, month + 1, 0).getDate();
  146. const daysInPrev = new Date(year, month, 0).getDate();
  147. let html = `
  148. <div class="month-calendar__header">
  149. <button class="month-calendar__arrow month-nav-prev" title="${t('prevMonth')}">
  150. <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>
  151. </button>
  152. <span class="month-calendar__title">${this.months[month] ?? ''} ${year}</span>
  153. <button class="month-calendar__arrow month-nav-next" title="${t('nextMonth')}">
  154. <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>
  155. </button>
  156. <button class="month-calendar__close week-nav__cal" title="${t('monthView')}">
  157. <svg viewBox="0 0 18 18" fill="none">
  158. <rect x="1" y="3" width="16" height="14" rx="2" stroke="currentColor" stroke-width="1.5"/>
  159. <path d="M1 7h16" stroke="currentColor" stroke-width="1.5"/>
  160. <path d="M5 1v4M13 1v4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
  161. <rect x="4" y="10" width="2" height="2" rx="0.5" fill="currentColor"/>
  162. <rect x="8" y="10" width="2" height="2" rx="0.5" fill="currentColor"/>
  163. <rect x="12" y="10" width="2" height="2" rx="0.5" fill="currentColor"/>
  164. </svg>
  165. </button>
  166. </div>
  167. <div class="month-calendar__grid">
  168. <div class="month-calendar__weekdays">
  169. ${this.weekdaysShort.map(d => `<span>${d}</span>`).join('')}
  170. </div>
  171. <div class="month-calendar__days">`;
  172. for (let i = startDow - 1; i >= 0; i--)
  173. html += `<span class="month-day month-day--other">${daysInPrev - i}</span>`;
  174. for (let d = 1; d <= daysInMonth; d++) {
  175. const date = new Date(year, month, d);
  176. const isToday = this.isSameDay(date, this.today);
  177. const isActive= this.isSameDay(date, this.activeDate);
  178. const cls = 'month-day'
  179. + (isToday ? ' month-day--today' : '')
  180. + (isActive ? ' month-day--active' : '');
  181. html += `<span class="${cls}" data-date="${this.formatDate(date)}">${d}</span>`;
  182. }
  183. const totalCells = Math.ceil((startDow + daysInMonth) / 7) * 7;
  184. for (let d = 1; d <= totalCells - startDow - daysInMonth; d++)
  185. html += `<span class="month-day month-day--other">${d}</span>`;
  186. html += `</div></div>`;
  187. this.monthEl.innerHTML = html;
  188. this.monthEl.querySelector('.month-nav-prev')?.addEventListener('click', () => this.navigateMonth(-1));
  189. this.monthEl.querySelector('.month-nav-next')?.addEventListener('click', () => this.navigateMonth(1));
  190. this.monthEl.querySelector('.month-calendar__close')?.addEventListener('click', () => this.closeMonth());
  191. this.monthEl.querySelectorAll('.month-day[data-date]').forEach(el => {
  192. el.addEventListener('click', () => this.goToDate(new Date(el.dataset.date + 'T00:00:00')));
  193. });
  194. }
  195. navigateMonth(direction) {
  196. const grid = this.monthEl?.querySelector('.month-calendar__grid');
  197. if (!grid) return;
  198. const slideOut = direction > 0 ? 'slide-out-left' : 'slide-out-right';
  199. grid.classList.add(slideOut);
  200. setTimeout(() => {
  201. this.monthDate.setMonth(this.monthDate.getMonth() + direction);
  202. this.renderMonthGrid();
  203. }, 160);
  204. }
  205. // ── Hilfsfunktionen ───────────────────────────────────────────────────────
  206. getMonday(date) {
  207. const d = new Date(date);
  208. const day = d.getDay();
  209. d.setDate(d.getDate() - day + (day === 0 ? -6 : 1));
  210. d.setHours(0, 0, 0, 0);
  211. return d;
  212. }
  213. isSameDay(a, b) {
  214. return a.getFullYear() === b.getFullYear()
  215. && a.getMonth() === b.getMonth()
  216. && a.getDate() === b.getDate();
  217. }
  218. formatDate(date) {
  219. const y = date.getFullYear();
  220. const m = String(date.getMonth() + 1).padStart(2, '0');
  221. const d = String(date.getDate()).padStart(2, '0');
  222. return `${y}-${m}-${d}`;
  223. }
  224. getWeekNumber(date) {
  225. const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
  226. d.setUTCDate(d.getUTCDate() + 4 - (d.getUTCDay() || 7));
  227. const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
  228. return Math.ceil((((d - yearStart) / 86400000) + 1) / 7);
  229. }
  230. }
  231. document.addEventListener('DOMContentLoaded', () => { new WeekCalendar(); });