// ── Calendar view ── import { $, esc } from './util.js'; import { state } from './state.js'; import { stripMd, getTodos } from './markdown.js'; const overlay = $('calendar'); const grid = $('cal-grid'); const title = $('cal-title'); const DAYS = ['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su']; const MONTHS = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']; let viewYear, viewMonth; // 0-indexed month function pad(n) { return String(n).padStart(2, '0'); } function dateStr(y, m, d) { return `${y}-${pad(m + 1)}-${pad(d)}`; } function cardDate(c) { // Title starts with date (journal "2026-04-08" or appointment "2026-04-08 — Meeting") const m = (c.title || '').match(/^(\d{4}-\d{2}-\d{2})/); if (m) return m[1]; // Fallback: created date return (c.created_at || '').substring(0, 10); } function getCardsForDate(ds) { return state.cards.filter(c => cardDate(c) === ds); } function isJournal(card) { return /^\d{4}-\d{2}-\d{2}/.test(card.title); } function render() { title.textContent = `${MONTHS[viewMonth]} ${viewYear}`; const firstDay = new Date(viewYear, viewMonth, 1).getDay(); // 0=Sun const startOffset = (firstDay + 6) % 7; // Mon=0 const daysInMonth = new Date(viewYear, viewMonth + 1, 0).getDate(); const prevMonthDays = new Date(viewYear, viewMonth, 0).getDate(); const today = new Date(); const todayStr = dateStr(today.getFullYear(), today.getMonth(), today.getDate()); // Always 6 rows = 42 cells for consistent layout const totalCells = 42; let html = '
'; for (const d of DAYS) html += `
${d}
`; html += '
'; html += '
'; for (let i = 0; i < totalCells; i++) { const dayNum = i - startOffset + 1; const inMonth = dayNum >= 1 && dayNum <= daysInMonth; let ds = '', displayNum = dayNum; if (dayNum < 1) { // Previous month displayNum = prevMonthDays + dayNum; const pm = viewMonth === 0 ? 11 : viewMonth - 1; const py = viewMonth === 0 ? viewYear - 1 : viewYear; ds = dateStr(py, pm, displayNum); } else if (dayNum > daysInMonth) { // Next month displayNum = dayNum - daysInMonth; const nm = viewMonth === 11 ? 0 : viewMonth + 1; const ny = viewMonth === 11 ? viewYear + 1 : viewYear; ds = dateStr(ny, nm, displayNum); } else { ds = dateStr(viewYear, viewMonth, dayNum); } const isToday = ds === todayStr; const cards = getCardsForDate(ds); const journals = cards.filter(isJournal); const others = cards.filter(c => !isJournal(c)); html += `
`; html += `
${displayNum}
`; html += '
'; for (const c of journals) { const body = stripMd(c.body || '').substring(0, 60); const todos = getTodos(c.body); html += `
`; if (todos) html += `${todos.done}/${todos.total}`; html += `${esc(body || '(empty)')}
`; } for (const c of others) { html += `
`; html += `${esc(c.title)}
`; } html += '
'; html += '
'; } html += '
'; grid.innerHTML = html; // Click on card item → open card grid.querySelectorAll('[data-card-id]').forEach(el => { el.addEventListener('click', e => { e.stopPropagation(); closeDayPopup(); state._returnToCalendar = true; window._openDetail(el.dataset.cardId); }); }); // Click on cell → open day popup grid.querySelectorAll('.cal-cell[data-date]').forEach(cell => { cell.addEventListener('click', e => { if (e.target.closest('[data-card-id]')) return; e.stopPropagation(); openDayPopup(cell.dataset.date, cell); }); }); } function navigate(delta) { closeDayPopup(); viewMonth += delta; if (viewMonth > 11) { viewMonth = 0; viewYear++; } if (viewMonth < 0) { viewMonth = 11; viewYear--; } render(); } // ── Day popup ── let dayPopup = null; function closeDayPopup() { if (dayPopup) { dayPopup.remove(); dayPopup = null; } } function openDayPopup(ds, cellEl) { closeDayPopup(); const cards = getCardsForDate(ds); const rect = cellEl.getBoundingClientRect(); dayPopup = document.createElement('div'); Object.assign(dayPopup.style, { position: 'fixed', zIndex: '200', background: 'var(--t-surface)', border: '1px solid var(--t-border)', boxShadow: '0 8px 32px rgba(0,0,0,0.4)', borderRadius: '4px', minWidth: '240px', maxWidth: '360px', maxHeight: '320px', display: 'flex', flexDirection: 'column', overflow: 'hidden', }); // Position near the cell const x = Math.min(rect.left + rect.width / 2 - 120, window.innerWidth - 370); const y = rect.bottom + 4 > window.innerHeight - 100 ? rect.top - 4 : rect.bottom + 4; dayPopup.style.left = Math.max(8, x) + 'px'; if (rect.bottom + 4 > window.innerHeight - 100) { dayPopup.style.bottom = (window.innerHeight - rect.top + 4) + 'px'; } else { dayPopup.style.top = y + 'px'; } // Header const header = document.createElement('div'); header.style.cssText = 'padding:8px 12px;border-bottom:1px solid var(--t-border);display:flex;align-items:center;gap:8px;flex-shrink:0;'; header.innerHTML = ` ${ds} ${cards.length} card${cards.length !== 1 ? 's' : ''} `; dayPopup.appendChild(header); // Card list const list = document.createElement('div'); list.style.cssText = 'overflow-y:auto;flex:1;padding:4px 0;'; if (cards.length) { list.innerHTML = cards.map(c => { const isJ = isJournal(c); const label = isJ ? (stripMd(c.body || '').substring(0, 80) || '(empty journal)') : c.title; const todos = getTodos(c.body); return `
${esc(label)} ${isJ ? 'journal' : ''} ${todos ? `${todos.done}/${todos.total}` : ''}
`; }).join(''); } else { list.innerHTML = '
no cards
'; } dayPopup.appendChild(list); document.body.appendChild(dayPopup); // Hover effect dayPopup.querySelectorAll('.day-popup-card').forEach(el => { el.addEventListener('mouseenter', () => { el.style.background = 'var(--t-raised)'; }); el.addEventListener('mouseleave', () => { el.style.background = ''; }); el.addEventListener('click', () => { closeDayPopup(); state._returnToCalendar = true; window._openDetail(el.dataset.cardId); }); }); // New card buttons dayPopup.querySelectorAll('.day-popup-new').forEach(btn => { btn.addEventListener('click', () => { const type = btn.dataset.type; closeDayPopup(); state._returnToCalendar = true; closeCalendar(); if (type === 'journal') { const existing = state.cards.find(c => c.title === ds); if (existing) { window._openDetail(existing.id); return; } window._openModal({ id: null, title: ds, body: '', color: '#38bdf8', bucket_id: null }); } else if (type === 'appointment') { const body = `**Zeit:** \n**Ort:** \n**Teilnehmer:** \n\n---\n\n**Notizen:**\n\n\n\n#termin`; window._openModal({ id: null, title: `${ds} — `, body, color: '#fb923c', bucket_id: null }); } }); }); // Close on click outside setTimeout(() => { const closeHandler = e => { if (dayPopup && !dayPopup.contains(e.target)) { closeDayPopup(); document.removeEventListener('click', closeHandler); } }; document.addEventListener('click', closeHandler); }, 0); } export function openCalendar() { const now = new Date(); viewYear = now.getFullYear(); viewMonth = now.getMonth(); render(); overlay.classList.add('open'); } export function closeCalendar() { closeDayPopup(); overlay.classList.remove('open'); } $('calendar-btn')?.addEventListener('click', openCalendar); $('cal-close')?.addEventListener('click', closeCalendar); $('cal-prev')?.addEventListener('click', () => navigate(-1)); $('cal-next')?.addEventListener('click', () => navigate(1)); $('cal-today')?.addEventListener('click', () => { const now = new Date(); viewYear = now.getFullYear(); viewMonth = now.getMonth(); render(); }); overlay?.addEventListener('click', e => { if (e.target === overlay) closeCalendar(); });