Ви не можете вибрати більше 25 тем Теми мають розпочинатися з літери або цифри, можуть містити дефіси (-) і не повинні перевищувати 35 символів.
 
 
 
 
 

282 рядки
11 KiB

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