240 lines
21 KiB
JavaScript
240 lines
21 KiB
JavaScript
'use strict';
|
||
// ---- Calendar: agenda built from private ICS links (managed here; fetched and parsed by the server)
|
||
const ymd = d => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
|
||
async function loadRange(start, days) {
|
||
const end = new Date(start); end.setDate(end.getDate() + days);
|
||
const last = new Date(end.getTime() - 1);
|
||
const q = new URLSearchParams({ from: start.getTime(), to: end.getTime() - 1, fd: ymd(start), td: ymd(last) });
|
||
const { events, errors } = await jfetch('/api/cal/events?' + q);
|
||
const byDay = new Map();
|
||
const put = (k, e) => { if (!byDay.has(k)) byDay.set(k, []); byDay.get(k).push(e); };
|
||
for (const e of events) {
|
||
if (e.allDay) { // spans start..end-1 (end exclusive)
|
||
for (let d = new Date(e.start + 'T00:00:00'); ymd(d) < e.end; d.setDate(d.getDate() + 1)) { const k = ymd(d); if (k >= ymd(start) && k <= ymd(last)) put(k, e); }
|
||
} else put(ymd(new Date(e.start)), e);
|
||
}
|
||
for (const list of byDay.values()) list.sort((a, b) => (b.allDay - a.allDay) || ((a.allDay ? 0 : a.start) - (b.allDay ? 0 : b.start)));
|
||
return { byDay, errors, start };
|
||
}
|
||
const loadAgenda = days => { const start = new Date(); start.setHours(0, 0, 0, 0); return loadRange(start, days); };
|
||
const fmtTime = ms => new Date(ms).toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' });
|
||
const evStore = []; // events currently on screen, so a tap can open the right one
|
||
const evRow = e => { const i = evStore.push(e) - 1;
|
||
return `<button class="row" data-ei="${i}" style="text-align:left"><span class="dot" style="background:${esc(e.color)}"></span><span class="grow"><b>${esc(e.title)}</b>
|
||
<small>${e.allDay ? 'All day' : fmtTime(e.start) + ' – ' + fmtTime(e.end)}${e.location ? ' · ' + esc(e.location) : ''} · ${esc(e.cal)}${e.series ? ' · repeats' : ''}</small></span>${e.editable ? icon('chev', 'chev') : ''}</button>`; };
|
||
document.addEventListener('click', e => { const b = e.target.closest('[data-ei]'); if (b) openEvent(evStore[+b.dataset.ei]); });
|
||
const dayLabel = k => { const d = new Date(k + 'T00:00:00'), t = ymd(new Date()); const tm = new Date(); tm.setDate(tm.getDate() + 1);
|
||
return (k === t ? 'Today · ' : k === ymd(tm) ? 'Tomorrow · ' : '') + d.toLocaleDateString([], { weekday: 'long', month: 'short', day: 'numeric' }); };
|
||
|
||
async function renderAgenda(el, mode) { // Home card (today only)
|
||
if (!el) return;
|
||
try {
|
||
const cals = await jfetch('/api/cal/list');
|
||
const { byDay } = await loadAgenda(1);
|
||
const list = byDay.get(ymd(new Date())) || [];
|
||
if (cals.length < 2 && !list.length) return; // only the empty NE Hub calendar: say nothing
|
||
if (!document.body.contains(el)) return;
|
||
el.innerHTML = `<h2>Today</h2><div class="list">${list.length ? list.slice(0, 6).map(evRow).join('') : '<div class="row"><small>Nothing on the calendar today</small></div>'}</div>`;
|
||
} catch {}
|
||
}
|
||
|
||
const now0 = new Date();
|
||
const calState = { y: now0.getFullYear(), m: now0.getMonth(), sel: ymd(now0) };
|
||
|
||
async function viewCalendar() {
|
||
const mode = store.get('cal_view', 'month');
|
||
const v = $('#view');
|
||
v.innerHTML = `<div class="wrap"><h1>Calendar</h1>
|
||
<div class="seg">${[['month', 'Month'], ['week', 'Week'], ['day', 'Day'], ['agenda', 'Agenda']].map(([k, l]) => `<a href="#" data-v="${k}" class="${k === mode ? 'on' : ''}">${l}</a>`).join('')}</div>
|
||
<div class="bar" style="margin:0 0 8px"><button class="btn" id="ne">${icon('cal')}Add event</button></div>
|
||
<div id="cbody"><p class="empty">Loading…</p></div><div class="bar"><button class="btn ghost" id="mc">Manage calendars</button></div></div>`;
|
||
$('#ne').onclick = () => newEvent(mode === 'agenda' ? ymd(new Date()) : calState.sel);
|
||
v.querySelectorAll('.seg a[data-v]').forEach(a => a.onclick = e => { e.preventDefault(); store.set('cal_view', a.dataset.v); viewCalendar(); });
|
||
$('#mc').onclick = manageCalendars;
|
||
try {
|
||
const cals = await jfetch('/api/cal/list');
|
||
if (!cals.length) { $('#cbody').innerHTML = '<p class="empty">No calendars yet. Add a private calendar link to see your events here.</p><div class="bar" style="justify-content:center"><button class="btn" id="ac">Add a calendar</button></div>'; $('#ac').onclick = addCalendar; return; }
|
||
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 = `<p class="err">${esc(e.message)}</p>`; }
|
||
}
|
||
|
||
async function renderAgendaList() {
|
||
const range = store.get('cal_range', 7);
|
||
const { byDay, errors } = await loadAgenda(range);
|
||
if (!location.hash.startsWith('#/calendar') || !$('#cbody')) return;
|
||
const keys = [...byDay.keys()].sort();
|
||
$('#cbody').innerHTML = `<div class="seg">${[[1, 'Today'], [7, '7 days'], [30, '30 days']].map(([n, l]) => `<a href="#" data-n="${n}" class="${n === range ? 'on' : ''}">${l}</a>`).join('')}</div>`
|
||
+ (keys.map(k => `<h2>${esc(dayLabel(k))}</h2><div class="list">${byDay.get(k).map(evRow).join('')}</div>`).join('') || '<p class="empty">Nothing coming up</p>')
|
||
+ errors.map(e => `<p class="err">${esc(e)}</p>`).join('');
|
||
$('#cbody').querySelectorAll('.seg a[data-n]').forEach(a => a.onclick = e => { e.preventDefault(); store.set('cal_range', +a.dataset.n); viewCalendar(); });
|
||
}
|
||
|
||
async function renderMonth() {
|
||
const { y, m } = calState;
|
||
const first = new Date(y, m, 1), gridStart = new Date(y, m, 1 - first.getDay()); // the Sunday on or before the 1st
|
||
const { byDay, errors } = await loadRange(gridStart, 42);
|
||
if (!location.hash.startsWith('#/calendar') || !$('#cbody')) return;
|
||
const today = ymd(new Date());
|
||
if (calState.sel < ymd(gridStart) || calState.sel > ymd(new Date(y, m, 1 - first.getDay() + 41))) calState.sel = ymd(first);
|
||
const cells = [];
|
||
for (let i = 0; i < 42; i++) {
|
||
const d = new Date(gridStart); d.setDate(d.getDate() + i);
|
||
const k = ymd(d), evs = byDay.get(k) || [], other = d.getMonth() !== m;
|
||
cells.push(`<button class="cday${other ? ' dim' : ''}${k === today ? ' today' : ''}${k === calState.sel ? ' sel' : ''}" data-d="${k}" aria-label="${esc(d.toDateString())}, ${evs.length} events">
|
||
<span class="dn">${d.getDate()}</span>
|
||
<span class="dots">${evs.slice(0, 4).map(e => `<i style="background:${esc(e.color)}"></i>`).join('')}</span>
|
||
<span class="chips">${evs.slice(0, 3).map(e => `<em style="--c:${esc(e.color)}">${esc(e.title)}</em>`).join('')}${evs.length > 3 ? `<small>+${evs.length - 3} more</small>` : ''}</span></button>`);
|
||
}
|
||
$('#cbody').innerHTML = `<div class="mhdr"><button class="ibtn" id="pm" aria-label="Previous month">${icon('back')}</button>
|
||
<h2 style="margin:0;flex:1;text-align:center;font-size:17px;text-transform:none;letter-spacing:0;color:var(--text)">${first.toLocaleDateString([], { month: 'long', year: 'numeric' })}</h2>
|
||
<button class="ibtn" id="nm" aria-label="Next month" style="transform:scaleX(-1)">${icon('back')}</button><button class="btn ghost" id="tm" style="min-height:36px">Today</button></div>
|
||
<div class="cgrid wk">${['S', 'M', 'T', 'W', 'T', 'F', 'S'].map(d => `<span>${d}</span>`).join('')}</div>
|
||
<div class="cgrid">${cells.join('')}</div>
|
||
<div id="dayev"></div>${errors.map(e => `<p class="err">${esc(e)}</p>`).join('')}`;
|
||
const showDay = () => {
|
||
const k = calState.sel, evs = byDay.get(k) || [];
|
||
$('#dayev').innerHTML = `<h2>${esc(dayLabel(k))}</h2><div class="list">${evs.length ? evs.map(evRow).join('') : '<div class="row"><small>No events</small></div>'}</div>`;
|
||
$('#cbody').querySelectorAll('.cday').forEach(c => c.classList.toggle('sel', c.dataset.d === k));
|
||
};
|
||
showDay();
|
||
$('#cbody').querySelectorAll('.cday').forEach(c => c.onclick = () => { calState.sel = c.dataset.d; showDay(); });
|
||
const go = (dm) => { const t = new Date(y, m + dm, 1); calState.y = t.getFullYear(); calState.m = t.getMonth(); calState.sel = ymd(t); renderMonth(); };
|
||
$('#pm').onclick = () => go(-1); $('#nm').onclick = () => go(1);
|
||
$('#tm').onclick = () => { const t = new Date(); calState.y = t.getFullYear(); calState.m = t.getMonth(); calState.sel = ymd(t); renderMonth(); };
|
||
}
|
||
|
||
const deviceTz = () => Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC';
|
||
const pad2 = n => String(n).padStart(2, '0');
|
||
const localStr = d => `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}T${pad2(d.getHours())}:${pad2(d.getMinutes())}`;
|
||
|
||
function openEvent(e) {
|
||
if (!e) return;
|
||
if (e.editable) return editEvent(e);
|
||
const when = e.allDay ? 'All day' : new Date(e.start).toLocaleString([], { dateStyle: 'medium', timeStyle: 'short' }) + ' – ' + fmtTime(e.end);
|
||
const el = openSheet(e.title, e.cal, `<div class="list" style="background:var(--panel2);border:0"><div class="row"><span class="grow"><b>When</b><small style="white-space:normal">${esc(when)}</small></span></div>
|
||
${e.location ? `<div class="row"><span class="grow"><b>Where</b><small style="white-space:normal">${esc(e.location)}</small></span></div>` : ''}</div>
|
||
<p class="sub">This calendar is read-only here (it is a subscribed link). Change it in the app it comes from.</p><div class="bar"><button class="btn ghost" id="sx" style="flex:1;justify-content:center">Close</button></div>`);
|
||
el.querySelector('#sx').onclick = closeSheet;
|
||
}
|
||
|
||
async function editEvent(e) {
|
||
try {
|
||
const q = new URLSearchParams({ cal: e.calId, tz: deviceTz() }); if (e.uid) q.set('uid', e.uid); if (e.href) q.set('href', e.href);
|
||
const r = await jfetch('/api/cal/event?' + q);
|
||
eventForm({ mode: 'edit', calId: e.calId, calName: e.cal, uid: e.uid, href: e.href || r.href, etag: r.etag || e.etag, fields: r.fields, series: e.series });
|
||
} catch (err) { toast(err.message); }
|
||
}
|
||
|
||
async function newEvent(day) {
|
||
try {
|
||
const cals = (await jfetch('/api/cal/list')).filter(c => c.local || c.icloud);
|
||
const d = new Date(day + 'T00:00:00'), now = new Date();
|
||
if (ymd(d) === ymd(now)) d.setHours(now.getHours() + 1, 0, 0, 0); else d.setHours(9, 0, 0, 0);
|
||
const end = new Date(d.getTime() + 36e5);
|
||
eventForm({ mode: 'new', cals, calId: store.get('cal_default', 'local'), fields: { title: '', location: '', notes: '', allDay: false, start: localStr(d), end: localStr(end), tz: deviceTz(), repeat: 'none' } });
|
||
} catch (err) { toast(err.message); }
|
||
}
|
||
|
||
function eventForm(o) {
|
||
const f = { ...o.fields }, editing = o.mode === 'edit';
|
||
const opts = [['none', 'Does not repeat'], ['daily', 'Every day'], ['weekdays', 'Every weekday'], ['weekly', 'Every week'], ['monthly', 'Every month'], ['yearly', 'Every year']];
|
||
if (f.repeat === 'keep') opts.push(['keep', 'Custom repeat (unchanged)']);
|
||
const calPick = editing ? `<div class="row" style="background:var(--panel2);border-radius:12px"><span class="grow"><small>Calendar</small><b>${esc(o.calName)}</b></span></div>`
|
||
: `<label for="fc">Calendar</label><select id="fc">${o.cals.map(c => `<option value="${esc(c.id)}" ${c.id === o.calId ? 'selected' : ''}>${esc(c.name)}${c.icloud ? ' (iCloud)' : ''}</option>`).join('')}</select>`;
|
||
const el = openSheet(editing ? 'Edit event' : 'New event', '', `<form id="ef"><input type="text" id="ft" placeholder="Title" value="${esc(f.title)}" autocomplete="off" required style="margin-top:6px">
|
||
<label class="chk"><input type="checkbox" id="fa" ${f.allDay ? 'checked' : ''}> All day</label>
|
||
<div id="fdt"></div>
|
||
<label for="fr">Repeat</label><select id="fr">${opts.map(([v, l]) => `<option value="${v}" ${v === f.repeat ? 'selected' : ''}>${l}</option>`).join('')}</select>
|
||
<label for="fl">Location</label><input type="text" id="fl" value="${esc(f.location)}" autocomplete="off">
|
||
<label for="fn">Notes</label><textarea id="fn" rows="3">${esc(f.notes)}</textarea>
|
||
${calPick}
|
||
${editing && o.series ? '<p class="sub" style="margin:8px 0 0">This repeats. Changes apply to the whole series.</p>' : ''}
|
||
<div class="bar"><button class="btn ghost" type="button" id="sx" style="flex:1;justify-content:center">Cancel</button><button class="btn" style="flex:1;justify-content:center">${editing ? 'Save' : 'Add'}</button></div>
|
||
${editing ? '<div class="bar" style="margin-top:0"><button class="btn danger" type="button" id="fd" style="flex:1;justify-content:center">Delete event</button></div>' : ''}
|
||
<div class="err" id="fe"></div></form>`);
|
||
el.querySelector('.sheet').classList.add('wide');
|
||
const draw = () => { // date-only inputs for all-day, date+time otherwise
|
||
const t = f.allDay ? 'date' : 'datetime-local', cut = v => f.allDay ? v.slice(0, 10) : v;
|
||
el.querySelector('#fdt').innerHTML = `<label for="fs">Starts</label><input type="${t}" id="fs" value="${cut(f.start)}" required><label for="fe2">Ends</label><input type="${t}" id="fe2" value="${cut(f.end)}" required>
|
||
${!f.allDay && f.tz !== deviceTz() ? `<p class="sub" style="margin:6px 0 0">Times are in ${esc(f.tz)}.</p>` : ''}`;
|
||
const s = el.querySelector('#fs'), en = el.querySelector('#fe2');
|
||
s.onchange = () => { // moving the start keeps the length
|
||
const len = Date.parse(f.allDay ? f.end.slice(0, 10) + 'T00:00' : f.end) - Date.parse(f.allDay ? f.start.slice(0, 10) + 'T00:00' : f.start);
|
||
f.start = s.value; if (s.value) { const ns = Date.parse(f.allDay ? s.value + 'T00:00' : s.value); if (!isNaN(ns) && len >= 0) { const d = new Date(ns + len); f.end = f.allDay ? ymd(d) : localStr(d); en.value = f.allDay ? ymd(d) : localStr(d); } }
|
||
};
|
||
en.onchange = () => { f.end = en.value; };
|
||
};
|
||
draw();
|
||
el.querySelector('#fa').onchange = ev => {
|
||
f.start = el.querySelector('#fs').value || f.start; f.end = el.querySelector('#fe2').value || f.end;
|
||
f.allDay = ev.target.checked;
|
||
if (f.allDay) { f.start = f.start.slice(0, 10); f.end = f.end.slice(0, 10); }
|
||
else { f.start = f.start.slice(0, 10) + 'T09:00'; f.end = f.end.slice(0, 10) + 'T10:00'; }
|
||
draw();
|
||
};
|
||
el.querySelector('#sx').onclick = closeSheet;
|
||
el.querySelector('#ef').onsubmit = async ev => {
|
||
ev.preventDefault(); const btn = el.querySelector('form .btn:not(.ghost):not(.danger)'); btn.disabled = true; el.querySelector('#fe').textContent = '';
|
||
const fields = { title: el.querySelector('#ft').value.trim(), location: el.querySelector('#fl').value.trim(), notes: el.querySelector('#fn').value.trim(), allDay: f.allDay,
|
||
start: el.querySelector('#fs').value, end: el.querySelector('#fe2').value, tz: f.tz, repeat: el.querySelector('#fr').value };
|
||
const cal = editing ? o.calId : el.querySelector('#fc').value;
|
||
try {
|
||
await jfetch('/api/cal/event', { method: 'POST', body: JSON.stringify({ cal, uid: o.uid, href: o.href, fields }) });
|
||
if (!editing) store.set('cal_default', cal);
|
||
closeSheet(); toast(editing ? 'Event saved' : 'Event added'); viewCalendar();
|
||
} catch (err) { btn.disabled = false; el.querySelector('#fe').textContent = err.message; }
|
||
};
|
||
const del = el.querySelector('#fd');
|
||
if (del) del.onclick = () => confirmSheet('Delete event?', o.series ? f.title + ' and every repeat of it' : f.title, 'Delete', async () => {
|
||
try { await jfetch('/api/cal/event/delete', { method: 'POST', body: JSON.stringify({ cal: o.calId, uid: o.uid, href: o.href, etag: o.etag }) }); toast('Event deleted'); viewCalendar(); }
|
||
catch (err) { toast(err.message); }
|
||
});
|
||
}
|
||
|
||
function addCalendar() {
|
||
const el = openSheet('Add a calendar link', '', `<form><label>Name</label><input type="text" id="cn" placeholder="Personal" autocomplete="off">
|
||
<label>Private calendar link (ICS)</label><input type="url" id="cu2" placeholder="https://… or webcal://…" autocomplete="off" autocapitalize="off" required>
|
||
<p class="sub" style="margin:10px 0 0">Google: Settings → your calendar → "Secret address in iCal format". Outlook.com / M365: Settings → Calendar → Shared calendars → Publish → copy the ICS link. These are read-only: you see the events here but change them in the original app. Anyone with the link can read that calendar, so keep it private.</p>
|
||
<div class="bar"><button class="btn ghost" type="button" id="sx" style="flex:1;justify-content:center">Cancel</button><button class="btn" style="flex:1;justify-content:center">Add</button></div><div class="err" id="ce"></div></form>`);
|
||
el.querySelector('#sx').onclick = closeSheet;
|
||
el.querySelector('form').onsubmit = async e => {
|
||
e.preventDefault(); const btn = el.querySelector('form .btn:not(.ghost)'); btn.disabled = true; el.querySelector('#ce').textContent = '';
|
||
try { await jfetch('/api/cal/add', { method: 'POST', body: JSON.stringify({ name: el.querySelector('#cn').value.trim() || 'Calendar', url: el.querySelector('#cu2').value.trim() }) }); closeSheet(); toast('Calendar added'); viewCalendar(); }
|
||
catch (err) { btn.disabled = false; el.querySelector('#ce').textContent = err.message; }
|
||
};
|
||
}
|
||
|
||
async function manageCalendars() {
|
||
const [cals, ic, feed] = await Promise.all([jfetch('/api/cal/list'), jfetch('/api/cal/icloud'), jfetch('/api/cal/feed')]);
|
||
const links = cals.filter(c => !c.local && !c.icloud);
|
||
const el = openSheet('Calendars', '', `<div class="list" style="background:var(--panel2);border:0">
|
||
<div class="row"><span class="dot" style="background:#4f7cff"></span><span class="grow"><b>NE Hub</b><small>Events you add here</small></span></div>
|
||
${links.map(c => `<div class="row"><span class="dot" style="background:${esc(c.color)}"></span><span class="grow"><b>${esc(c.name)}</b><small>Link (read-only)</small></span><button class="btn danger" data-rm="${esc(c.id)}" style="min-height:36px">Remove</button></div>`).join('')}
|
||
</div>
|
||
<div class="bar"><button class="btn ghost" id="al" style="flex:1;justify-content:center">Add a calendar link</button></div>
|
||
<h2 style="margin-top:14px">iCloud calendars</h2>
|
||
${ic.connected ? `<div class="list" style="background:var(--panel2);border:0">${ic.calendars.map(c => `<label class="row" style="cursor:pointer"><span class="dot" style="background:${esc(c.color)}"></span><span class="grow"><b>${esc(c.name)}</b></span><input type="checkbox" data-ic="${esc(c.id)}" ${c.enabled ? 'checked' : ''}></label>`).join('')}</div>
|
||
<p class="sub">Signed in as ${esc(ic.appleId)}. Events you add or change here are saved to iCloud.</p><div class="bar"><button class="btn danger" id="icx" style="flex:1;justify-content:center">Disconnect iCloud</button></div>`
|
||
: `<p class="sub">Read and add events in your iCloud calendars. Use an <b>app-specific password</b> (appleid.apple.com → Sign-In and Security → App-Specific Passwords), not your normal Apple password.</p>
|
||
<form id="icf"><input type="text" id="iu" placeholder="Apple ID (email)" autocomplete="off" autocapitalize="off" required><input type="password" id="ip" placeholder="App-specific password" autocomplete="off" style="margin-top:8px" required>
|
||
<div class="bar"><button class="btn" style="flex:1;justify-content:center">Connect iCloud</button></div><div class="err" id="ice"></div></form>`}
|
||
<h2 style="margin-top:14px">See NE Hub events in other apps</h2>
|
||
<p class="sub">Subscribe to this private link from Outlook, Google or Apple Calendar. They show your NE Hub events (read-only there).</p>
|
||
<input type="text" readonly id="fu" value="${esc(feed.url)}" onfocus="this.select()">
|
||
<div class="bar"><button class="btn ghost" id="fc2" style="flex:1;justify-content:center">${icon('copy')}Copy link</button><button class="btn ghost" id="fr2" style="flex:1;justify-content:center">Make a new link</button></div>
|
||
<div class="bar"><button class="btn ghost" id="sx" style="flex:1;justify-content:center">Close</button></div>`);
|
||
el.querySelector('.sheet').classList.add('wide');
|
||
el.querySelector('#sx').onclick = closeSheet; el.querySelector('#al').onclick = addCalendar;
|
||
el.querySelectorAll('[data-rm]').forEach(b => b.onclick = async () => { await jfetch('/api/cal/remove', { method: 'POST', body: JSON.stringify({ id: b.dataset.rm }) }); closeSheet(); viewCalendar(); });
|
||
el.querySelectorAll('[data-ic]').forEach(c => c.onchange = async () => { try { await jfetch('/api/cal/icloud/toggle', { method: 'POST', body: JSON.stringify({ id: c.dataset.ic, enabled: c.checked }) }); } catch (e) { toast(e.message); } });
|
||
const icx = el.querySelector('#icx'); if (icx) icx.onclick = async () => { await jfetch('/api/cal/icloud/disconnect', { method: 'POST', body: '{}' }); toast('iCloud disconnected'); closeSheet(); viewCalendar(); };
|
||
const icf = el.querySelector('#icf');
|
||
if (icf) icf.onsubmit = async e => {
|
||
e.preventDefault(); const btn = icf.querySelector('.btn'); btn.disabled = true; btn.textContent = 'Connecting…'; el.querySelector('#ice').textContent = '';
|
||
try { await jfetch('/api/cal/icloud/connect', { method: 'POST', body: JSON.stringify({ appleId: el.querySelector('#iu').value, password: el.querySelector('#ip').value }) }); toast('iCloud connected'); closeSheet(); viewCalendar(); }
|
||
catch (err) { btn.disabled = false; btn.textContent = 'Connect iCloud'; el.querySelector('#ice').textContent = err.message; }
|
||
};
|
||
el.querySelector('#fc2').onclick = async () => { try { await navigator.clipboard.writeText(feed.url); toast('Link copied'); } catch { el.querySelector('#fu').select(); toast('Press copy on the selected link'); } };
|
||
el.querySelector('#fr2').onclick = () => confirmSheet('Make a new link?', 'The old link stops working, so anything subscribed to it has to be updated.', 'Make new link', async () => { await jfetch('/api/cal/feed', { method: 'POST', body: '{}' }); manageCalendars(); });
|
||
}
|