diff --git a/public/app.js b/public/app.js index f1b1d72..62ffe71 100644 --- a/public/app.js +++ b/public/app.js @@ -448,16 +448,16 @@ async function viewCalendar() { const mode = store.get('cal_view', 'month'); const v = $('#view'); v.innerHTML = `
Loading…
No calendars yet. Add a private calendar link to see your events here.
'; $('#ac').onclick = addCalendar; return; } - if (mode === 'month') await renderMonth(); else await renderAgendaList(); + if (mode === 'month') await renderMonth(); else if (mode === 'week') await renderTimeGrid(7); else if (mode === 'day') await renderTimeGrid(1); else await renderAgendaList(); } catch (e) { const b = $('#cbody'); if (b) b.innerHTML = `${esc(e.message)}
`; } } diff --git a/public/calendar-views.js b/public/calendar-views.js new file mode 100644 index 0000000..3689cbb --- /dev/null +++ b/public/calendar-views.js @@ -0,0 +1,50 @@ +// Calendar week and day views: a time grid with all-day strip, overlapping events side by side and a "now" line. +// Uses the calendar globals from app.js (calState, loadRange, evStore, ymd, fmtTime, esc, icon, newEvent, viewCalendar). +const HOUR_PX = 48; + +/** Lay out one day's timed events in columns so overlapping ones sit side by side. */ +function layoutDay(events) { + const evs = events.map(e => ({ e, s: e.sm, en: Math.max(e.em, e.sm + 20) })).sort((a, b) => a.s - b.s || b.en - a.en); + const out = []; let cluster = [], clusterEnd = -1; + const flush = () => { const cols = []; for (const it of cluster) { let c = cols.findIndex(end => end <= it.s); if (c < 0) { c = cols.length; cols.push(0); } cols[c] = it.en; it.col = c; } cluster.forEach(it => { it.cols = cols.length; out.push(it); }); cluster = []; }; + for (const it of evs) { if (cluster.length && it.s >= clusterEnd) { flush(); clusterEnd = -1; } cluster.push(it); clusterEnd = Math.max(clusterEnd, it.en); } + if (cluster.length) flush(); + return out; +} + +async function renderTimeGrid(nDays) { + const sel = new Date(calState.sel + 'T00:00:00'), start = new Date(sel); + if (nDays === 7) start.setDate(start.getDate() - start.getDay()); + const { byDay, errors } = await loadRange(start, nDays); + if (!location.hash.startsWith('#/calendar') || !$('#cbody')) return; + evStore.length = 0; + const today = ymd(new Date()), days = [...Array(nDays)].map((_, i) => { const d = new Date(start); d.setDate(d.getDate() + i); return d; }); + const title = nDays === 1 ? sel.toLocaleDateString([], { weekday: 'long', month: 'long', day: 'numeric', year: 'numeric' }) + : days[0].toLocaleDateString([], { month: 'short', day: 'numeric' }) + ' – ' + days[6].toLocaleDateString([], { month: 'short', day: 'numeric', year: 'numeric' }); + const allDay = days.map(d => (byDay.get(ymd(d)) || []).filter(e => e.allDay)); + const cols = days.map(d => { + const k = ymd(d), t0 = new Date(k + 'T00:00:00').getTime(), t1 = t0 + 864e5; + const timed = (byDay.get(k) || []).filter(e => !e.allDay).map(e => ({ ...e, sm: (Math.max(e.start, t0) - t0) / 6e4, em: (Math.min(e.end, t1) - t0) / 6e4, orig: e })); + return layoutDay(timed).map(({ e, s, en, col, cols: n }) => { + const i = evStore.push(e.orig) - 1, h = Math.max(18, (en - s) / 60 * HOUR_PX - 2); + return ``; + }).join(''); + }); + const hours = [...Array(24)].map((_, h) => `${esc(e)}
`).join('')}`; + const body = $('#tgbody'); if (body) { const wrap = body.parentElement.parentElement; wrap.scrollTop = Math.max(0, ((days.some(d => ymd(d) === today) ? now.getHours() - 1 : 7) * HOUR_PX)); } + const shift = n => { const d = new Date(sel); d.setDate(d.getDate() + n * (nDays === 1 ? 1 : 7)); calState.sel = ymd(d); calState.y = d.getFullYear(); calState.m = d.getMonth(); renderTimeGrid(nDays); }; + $('#tp').onclick = () => shift(-1); $('#tn').onclick = () => shift(1); + $('#tt').onclick = () => { const d = new Date(); calState.sel = ymd(d); calState.y = d.getFullYear(); calState.m = d.getMonth(); renderTimeGrid(nDays); }; + $('#cbody').querySelectorAll('.tgd').forEach(b => b.onclick = () => { if (nDays === 7) { calState.sel = b.dataset.day; store.set('cal_view', 'day'); viewCalendar(); } }); + $('#cbody').querySelectorAll('.tgcol').forEach(c => c.onclick = e => { if (e.target === c) newEvent(c.dataset.day); }); +} diff --git a/public/index.html b/public/index.html index 68dca8d..09d2ea2 100644 --- a/public/index.html +++ b/public/index.html @@ -55,6 +55,7 @@ +