Add birthday feature (web)
- views render display_title (age) and a cake icon for is_birthday events (month/week/agenda + quarter tooltip) via shared eventTitle/birthdayIconSvg - create split-button caret opens a menu: new event / new birthday - new birthday modal: name + date + "year unknown" + target birthday calendar, saved as an all-day FREQ=YEARLY local event with birth_year - local-calendar modal gains a "birthday calendar" toggle + "notify N days before" - i18n de/en strings Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -165,6 +165,15 @@ a { color: var(--primary); text-decoration: none; }
|
|||||||
}
|
}
|
||||||
.btn-fab:active { transform: translateY(0) scale(.985); }
|
.btn-fab:active { transform: translateY(0) scale(.985); }
|
||||||
|
|
||||||
|
/* Split create button: main "Erstellen" action + a caret that opens a small
|
||||||
|
menu (new event / new birthday). */
|
||||||
|
.create-split { display: flex; align-items: stretch; gap: 4px; margin: 16px 12px 8px; }
|
||||||
|
.create-split .btn-fab { margin: 0; }
|
||||||
|
.create-split .create-main { flex: 1; justify-content: center; }
|
||||||
|
.create-split .create-caret { flex: 0 0 auto; padding: 12px 12px; }
|
||||||
|
.create-split .create-caret svg { width: 18px; height: 18px; }
|
||||||
|
.create-menu { left: 0; right: auto; min-width: 200px; }
|
||||||
|
|
||||||
/* Circular icon buttons (topbar nav, modal close, etc.) */
|
/* Circular icon buttons (topbar nav, modal close, etc.) */
|
||||||
.icon-btn {
|
.icon-btn {
|
||||||
display: inline-flex; align-items: center; justify-content: center;
|
display: inline-flex; align-items: center; justify-content: center;
|
||||||
|
|||||||
@@ -158,10 +158,19 @@
|
|||||||
<!-- SIDEBAR -->
|
<!-- SIDEBAR -->
|
||||||
<aside class="sidebar" id="sidebar">
|
<aside class="sidebar" id="sidebar">
|
||||||
<div class="sidebar-inner">
|
<div class="sidebar-inner">
|
||||||
<button class="btn btn-fab" id="btn-create-event">
|
<div class="create-split add-cal-dropdown-wrap">
|
||||||
<svg viewBox="0 0 24 24" fill="currentColor" width="24" height="24"><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>
|
<button class="btn btn-fab create-main" id="btn-create-event">
|
||||||
<span data-i18n="btn_create">Erstellen</span>
|
<svg viewBox="0 0 24 24" fill="currentColor" width="24" height="24"><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>
|
||||||
</button>
|
<span data-i18n="btn_create">Erstellen</span>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-fab create-caret" id="btn-create-menu" data-i18n-title="birthday_new" title="Neuer Geburtstag" aria-haspopup="menu" aria-expanded="false">
|
||||||
|
<svg viewBox="0 0 24 24" fill="currentColor" width="18" height="18"><path d="M7 10l5 5 5-5z"/></svg>
|
||||||
|
</button>
|
||||||
|
<div class="add-cal-dropdown create-menu hidden" id="create-menu" role="menu">
|
||||||
|
<button data-action="event" data-i18n="create_event_option">Neuer Termin</button>
|
||||||
|
<button data-action="birthday" data-i18n="birthday_new">Neuer Geburtstag</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Mini Calendar -->
|
<!-- Mini Calendar -->
|
||||||
<div class="mini-cal" id="mini-cal">
|
<div class="mini-cal" id="mini-cal">
|
||||||
@@ -581,6 +590,17 @@
|
|||||||
<div class="ev-color-preview" id="local-cal-color-preview" style="background:#34a853" title="Farbe wählen"></div>
|
<div class="ev-color-preview" id="local-cal-color-preview" style="background:#34a853" title="Farbe wählen"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="checkbox-row">
|
||||||
|
<input type="checkbox" id="local-cal-birthday" />
|
||||||
|
<span data-i18n="birthday_calendar">Geburtstagskalender</span>
|
||||||
|
</label>
|
||||||
|
<div class="form-hint" id="local-cal-birthday-hint" data-i18n="birthday_calendar_hint">Ganztägige, jährliche Termine mit Alter und Geburtstags-Icon.</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group hidden" id="local-cal-notify-group">
|
||||||
|
<label data-i18n="birthday_notify">Erinnerung</label>
|
||||||
|
<select id="local-cal-notify"></select>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button class="btn btn-ghost" data-modal="modal-local-cal">Abbrechen</button>
|
<button class="btn btn-ghost" data-modal="modal-local-cal">Abbrechen</button>
|
||||||
@@ -589,6 +609,39 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Birthday modal -->
|
||||||
|
<div id="modal-birthday" class="modal-overlay hidden">
|
||||||
|
<div class="modal-card" style="max-width:400px">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h3 data-i18n="birthday_modal_title">Neuen Geburtstag hinzufügen</h3>
|
||||||
|
<button class="icon-btn modal-close" data-modal="modal-birthday">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div id="birthday-no-cals" class="form-hint hidden" data-i18n="birthday_no_calendars"></div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label data-i18n="birthday_person">Name</label>
|
||||||
|
<input type="text" id="birthday-name" data-i18n-ph="birthday_person_ph" placeholder="Name der Person" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label data-i18n="birthday_date">Geburtstag</label>
|
||||||
|
<input type="date" id="birthday-date" />
|
||||||
|
<label class="checkbox-inline" style="margin-top:8px">
|
||||||
|
<input type="checkbox" id="birthday-year-unknown" />
|
||||||
|
<span data-i18n="birthday_year_unknown">Jahr unbekannt</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-group" id="birthday-cal-group">
|
||||||
|
<label data-i18n="birthday_target">Geburtstagskalender</label>
|
||||||
|
<select id="birthday-cal"></select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button class="btn btn-ghost" data-modal="modal-birthday" data-i18n="cancel">Abbrechen</button>
|
||||||
|
<button class="btn btn-primary" id="birthday-save" data-i18n="save">Speichern</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- iCal Subscription Modal -->
|
<!-- iCal Subscription Modal -->
|
||||||
<div id="modal-ical-sub" class="modal-overlay hidden">
|
<div id="modal-ical-sub" class="modal-overlay hidden">
|
||||||
<div class="modal-card" style="max-width:480px">
|
<div class="modal-card" style="max-width:480px">
|
||||||
|
|||||||
@@ -155,6 +155,7 @@ export async function initCalendar() {
|
|||||||
bindEventModal();
|
bindEventModal();
|
||||||
bindAccountModal();
|
bindAccountModal();
|
||||||
bindLocalCalModal();
|
bindLocalCalModal();
|
||||||
|
bindBirthdayModal();
|
||||||
bindICalSubModal();
|
bindICalSubModal();
|
||||||
bindHAAccountModal();
|
bindHAAccountModal();
|
||||||
bindSettingsModal();
|
bindSettingsModal();
|
||||||
@@ -1240,6 +1241,29 @@ function bindTopbar() {
|
|||||||
const fab = document.getElementById('btn-create-fab');
|
const fab = document.getElementById('btn-create-fab');
|
||||||
if (fab) fab.onclick = () => openNewEventModal(state.selectedDate || state.currentDate);
|
if (fab) fab.onclick = () => openNewEventModal(state.selectedDate || state.currentDate);
|
||||||
|
|
||||||
|
// Create split-button caret → menu (new event / new birthday)
|
||||||
|
const createMenuBtn = document.getElementById('btn-create-menu');
|
||||||
|
const createMenu = document.getElementById('create-menu');
|
||||||
|
if (createMenuBtn && createMenu) {
|
||||||
|
createMenuBtn.onclick = e => {
|
||||||
|
e.stopPropagation();
|
||||||
|
createMenu.classList.toggle('hidden');
|
||||||
|
};
|
||||||
|
document.addEventListener('click', e => {
|
||||||
|
if (!createMenu.contains(e.target) && e.target !== createMenuBtn && !createMenuBtn.contains(e.target)) {
|
||||||
|
createMenu.classList.add('hidden');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
createMenu.querySelector('[data-action="event"]').onclick = () => {
|
||||||
|
createMenu.classList.add('hidden');
|
||||||
|
openNewEventModal(state.selectedDate || state.currentDate);
|
||||||
|
};
|
||||||
|
createMenu.querySelector('[data-action="birthday"]').onclick = () => {
|
||||||
|
createMenu.classList.add('hidden');
|
||||||
|
openBirthdayModal();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// Mobile view-toggle popup
|
// Mobile view-toggle popup
|
||||||
const viewMobileBtn = document.getElementById('btn-view-mobile');
|
const viewMobileBtn = document.getElementById('btn-view-mobile');
|
||||||
const viewMobileDropdown = document.getElementById('view-mobile-dropdown');
|
const viewMobileDropdown = document.getElementById('view-mobile-dropdown');
|
||||||
@@ -2499,17 +2523,37 @@ function bindAccountModal() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Birthday reminder options (shared by the calendar & birthday modals) ──
|
||||||
|
// value -1 = off, 0 = on the day, N = N days before.
|
||||||
|
function fillBirthdayNotifySelect(sel) {
|
||||||
|
sel.innerHTML =
|
||||||
|
`<option value="-1">${escHtml(t('birthday_notify_off'))}</option>` +
|
||||||
|
`<option value="0">${escHtml(t('birthday_notify_same_day'))}</option>` +
|
||||||
|
`<option value="1">${escHtml(t('birthday_notify_one_day'))}</option>` +
|
||||||
|
[2, 3, 7].map(d => `<option value="${d}">${escHtml(t('birthday_notify_days', { n: d }))}</option>`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
// ── Local Calendar Modal ──────────────────────────────────
|
// ── Local Calendar Modal ──────────────────────────────────
|
||||||
function openLocalCalModal() {
|
function openLocalCalModal() {
|
||||||
document.getElementById('local-cal-name').value = '';
|
document.getElementById('local-cal-name').value = '';
|
||||||
document.getElementById('local-cal-color-hex').value = '#34a853';
|
document.getElementById('local-cal-color-hex').value = '#34a853';
|
||||||
document.getElementById('local-cal-color-preview').style.background = '#34a853';
|
document.getElementById('local-cal-color-preview').style.background = '#34a853';
|
||||||
|
const bday = document.getElementById('local-cal-birthday');
|
||||||
|
bday.checked = false;
|
||||||
|
document.getElementById('local-cal-notify').value = '-1';
|
||||||
|
document.getElementById('local-cal-notify-group').classList.add('hidden');
|
||||||
openModal('modal-local-cal');
|
openModal('modal-local-cal');
|
||||||
}
|
}
|
||||||
|
|
||||||
function bindLocalCalModal() {
|
function bindLocalCalModal() {
|
||||||
const preview = document.getElementById('local-cal-color-preview');
|
const preview = document.getElementById('local-cal-color-preview');
|
||||||
const hex = document.getElementById('local-cal-color-hex');
|
const hex = document.getElementById('local-cal-color-hex');
|
||||||
|
const bday = document.getElementById('local-cal-birthday');
|
||||||
|
const notifyGroup = document.getElementById('local-cal-notify-group');
|
||||||
|
fillBirthdayNotifySelect(document.getElementById('local-cal-notify'));
|
||||||
|
bday.addEventListener('change', () => {
|
||||||
|
notifyGroup.classList.toggle('hidden', !bday.checked);
|
||||||
|
});
|
||||||
preview.addEventListener('click', async () => {
|
preview.addEventListener('click', async () => {
|
||||||
const picked = await openColorPicker(preview, hex.value || '#34a853');
|
const picked = await openColorPicker(preview, hex.value || '#34a853');
|
||||||
if (picked) { hex.value = picked.toUpperCase(); preview.style.background = picked; }
|
if (picked) { hex.value = picked.toUpperCase(); preview.style.background = picked; }
|
||||||
@@ -2524,8 +2568,15 @@ function bindLocalCalModal() {
|
|||||||
const name = document.getElementById('local-cal-name').value.trim();
|
const name = document.getElementById('local-cal-name').value.trim();
|
||||||
if (!name) { showToast(t('error_enter_name'), true); return; }
|
if (!name) { showToast(t('error_enter_name'), true); return; }
|
||||||
const color = hex.value;
|
const color = hex.value;
|
||||||
|
const isBirthday = bday.checked;
|
||||||
|
const notify = parseInt(document.getElementById('local-cal-notify').value, 10);
|
||||||
|
const body = { name, color };
|
||||||
|
if (isBirthday) {
|
||||||
|
body.is_birthday = true;
|
||||||
|
if (notify >= 0) body.birthday_notify_days_before = notify;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const cal = await api.post('/local/calendars', { name, color });
|
const cal = await api.post('/local/calendars', body);
|
||||||
state.localCalendars.push(cal);
|
state.localCalendars.push(cal);
|
||||||
renderCalendarList();
|
renderCalendarList();
|
||||||
closeModal('modal-local-cal');
|
closeModal('modal-local-cal');
|
||||||
@@ -2534,6 +2585,62 @@ function bindLocalCalModal() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Birthday Modal (manual entry) ─────────────────────────
|
||||||
|
function birthdayCalendars() {
|
||||||
|
return (state.localCalendars || []).filter(
|
||||||
|
c => c.is_birthday && (c.owned !== false || c.permission === 'read_write')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function openBirthdayModal() {
|
||||||
|
const cals = birthdayCalendars();
|
||||||
|
const sel = document.getElementById('birthday-cal');
|
||||||
|
sel.innerHTML = cals.map(c => `<option value="${c.id}">${escHtml(c.name)}</option>`).join('');
|
||||||
|
const none = cals.length === 0;
|
||||||
|
document.getElementById('birthday-no-cals').classList.toggle('hidden', !none);
|
||||||
|
document.getElementById('birthday-cal-group').classList.toggle('hidden', none || cals.length === 1);
|
||||||
|
document.getElementById('birthday-name').value = '';
|
||||||
|
document.getElementById('birthday-year-unknown').checked = false;
|
||||||
|
const dateEl = document.getElementById('birthday-date');
|
||||||
|
const today = new Date();
|
||||||
|
dateEl.value = `${today.getFullYear()}-${String(today.getMonth()+1).padStart(2,'0')}-${String(today.getDate()).padStart(2,'0')}`;
|
||||||
|
document.getElementById('birthday-save').disabled = none;
|
||||||
|
openModal('modal-birthday');
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindBirthdayModal() {
|
||||||
|
document.getElementById('birthday-save').onclick = async () => {
|
||||||
|
const cals = birthdayCalendars();
|
||||||
|
if (!cals.length) return;
|
||||||
|
const name = document.getElementById('birthday-name').value.trim();
|
||||||
|
if (!name) { showToast(t('error_enter_name'), true); return; }
|
||||||
|
const dateStr = document.getElementById('birthday-date').value; // YYYY-MM-DD
|
||||||
|
if (!dateStr) return;
|
||||||
|
const [y, m, d] = dateStr.split('-').map(n => parseInt(n, 10));
|
||||||
|
const yearUnknown = document.getElementById('birthday-year-unknown').checked;
|
||||||
|
const calSel = document.getElementById('birthday-cal');
|
||||||
|
const calId = parseInt(calSel.value || cals[0].id, 10);
|
||||||
|
// All-day anchor: birth year if known, else 1970 so FREQ=YEARLY expands
|
||||||
|
// across any queried range. end = next day.
|
||||||
|
const anchorYear = yearUnknown ? 1970 : y;
|
||||||
|
const pad = n => String(n).padStart(2, '0');
|
||||||
|
const start = `${anchorYear}-${pad(m)}-${pad(d)}`;
|
||||||
|
const endDate = new Date(Date.UTC(anchorYear, m - 1, d + 1));
|
||||||
|
const end = `${endDate.getUTCFullYear()}-${pad(endDate.getUTCMonth()+1)}-${pad(endDate.getUTCDate())}`;
|
||||||
|
const body = {
|
||||||
|
calendar_id: calId, title: name, start, end, allDay: true,
|
||||||
|
rrule: 'FREQ=YEARLY',
|
||||||
|
};
|
||||||
|
if (!yearUnknown) body.birth_year = y;
|
||||||
|
try {
|
||||||
|
await api.post('/local/events', body);
|
||||||
|
closeModal('modal-birthday');
|
||||||
|
showToast(t('birthday_created', { name }));
|
||||||
|
fetchAndRender();
|
||||||
|
} catch (e) { showToast(e.message, true); }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// ── Home Assistant Account Modal ─────────────────────────
|
// ── Home Assistant Account Modal ─────────────────────────
|
||||||
function openHAAccountModal() {
|
function openHAAccountModal() {
|
||||||
document.getElementById('ha-account-name').value = '';
|
document.getElementById('ha-account-name').value = '';
|
||||||
|
|||||||
@@ -272,6 +272,24 @@ const translations = {
|
|||||||
account_added: 'Konto "{name}" hinzugefügt',
|
account_added: 'Konto "{name}" hinzugefügt',
|
||||||
error_enter_name: 'Bitte Name eingeben',
|
error_enter_name: 'Bitte Name eingeben',
|
||||||
calendar_created: 'Kalender "{name}" erstellt',
|
calendar_created: 'Kalender "{name}" erstellt',
|
||||||
|
// Birthdays
|
||||||
|
create_event_option: 'Neuer Termin',
|
||||||
|
birthday_new: 'Neuer Geburtstag',
|
||||||
|
birthday_modal_title: 'Neuen Geburtstag hinzufügen',
|
||||||
|
birthday_calendar: 'Geburtstagskalender',
|
||||||
|
birthday_calendar_hint: 'Ganztägige, jährliche Termine mit Alter und Geburtstags-Icon.',
|
||||||
|
birthday_notify: 'Erinnerung',
|
||||||
|
birthday_notify_off: 'Aus',
|
||||||
|
birthday_notify_same_day: 'Am Tag',
|
||||||
|
birthday_notify_one_day: '1 Tag vorher',
|
||||||
|
birthday_notify_days: '{n} Tage vorher',
|
||||||
|
birthday_person: 'Name',
|
||||||
|
birthday_person_ph: 'Name der Person',
|
||||||
|
birthday_date: 'Geburtstag',
|
||||||
|
birthday_year_unknown: 'Jahr unbekannt',
|
||||||
|
birthday_target: 'Geburtstagskalender',
|
||||||
|
birthday_no_calendars: 'Kein Geburtstagskalender vorhanden. Erstelle zuerst einen Kalender und aktiviere „Geburtstagskalender".',
|
||||||
|
birthday_created: 'Geburtstag „{name}" hinzugefügt',
|
||||||
error_name_url: 'Bitte Name und URL eingeben',
|
error_name_url: 'Bitte Name und URL eingeben',
|
||||||
ical_subscribed: '"{name}" abonniert',
|
ical_subscribed: '"{name}" abonniert',
|
||||||
google_synced: 'Kalender synchronisiert',
|
google_synced: 'Kalender synchronisiert',
|
||||||
@@ -589,6 +607,24 @@ const translations = {
|
|||||||
account_added: 'Account "{name}" added',
|
account_added: 'Account "{name}" added',
|
||||||
error_enter_name: 'Please enter a name',
|
error_enter_name: 'Please enter a name',
|
||||||
calendar_created: 'Calendar "{name}" created',
|
calendar_created: 'Calendar "{name}" created',
|
||||||
|
// Birthdays
|
||||||
|
create_event_option: 'New event',
|
||||||
|
birthday_new: 'New birthday',
|
||||||
|
birthday_modal_title: 'Add new birthday',
|
||||||
|
birthday_calendar: 'Birthday calendar',
|
||||||
|
birthday_calendar_hint: 'All-day, yearly events with age and a birthday icon.',
|
||||||
|
birthday_notify: 'Reminder',
|
||||||
|
birthday_notify_off: 'Off',
|
||||||
|
birthday_notify_same_day: 'On the day',
|
||||||
|
birthday_notify_one_day: '1 day before',
|
||||||
|
birthday_notify_days: '{n} days before',
|
||||||
|
birthday_person: 'Name',
|
||||||
|
birthday_person_ph: "Person's name",
|
||||||
|
birthday_date: 'Birthday',
|
||||||
|
birthday_year_unknown: 'Year unknown',
|
||||||
|
birthday_target: 'Birthday calendar',
|
||||||
|
birthday_no_calendars: 'No birthday calendar yet. Create a calendar and enable “Birthday calendar” first.',
|
||||||
|
birthday_created: 'Birthday “{name}” added',
|
||||||
error_name_url: 'Please enter a name and URL',
|
error_name_url: 'Please enter a name and URL',
|
||||||
ical_subscribed: '"{name}" subscribed',
|
ical_subscribed: '"{name}" subscribed',
|
||||||
google_synced: 'Calendar synced',
|
google_synced: 'Calendar synced',
|
||||||
|
|||||||
@@ -16,6 +16,26 @@ export function isPast(ev) {
|
|||||||
return end < new Date();
|
return end < new Date();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Title to render for an event: the server-decorated one (birthday age, group
|
||||||
|
// prefix) wins over the raw title, which stays untouched for editing.
|
||||||
|
export function eventTitle(ev) {
|
||||||
|
return ev.display_title || ev.title || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Small inline cake icon for birthday events, sized to the surrounding text and
|
||||||
|
// tinted with the current text colour so it matches every bar it's dropped into.
|
||||||
|
export function birthdayIconSvg() {
|
||||||
|
return '<svg viewBox="0 0 24 24" aria-hidden="true" '
|
||||||
|
+ 'style="width:0.85em;height:0.85em;vertical-align:-0.1em;margin-right:2px;flex:0 0 auto">'
|
||||||
|
+ '<path fill="currentColor" d="M12 6c1.11 0 2-.9 2-2 0-.38-.1-.73-.29-1.03L12 0l-1.71 2.97'
|
||||||
|
+ 'c-.19.3-.29.65-.29 1.03 0 1.1.9 2 2 2zm4.6 9.99l-1.07-1.07-1.08 1.07c-1.3 1.3-3.58 1.31-4.89 0'
|
||||||
|
+ 'l-1.07-1.07-1.09 1.07C6.75 16.64 5.88 17 4.96 17c-.73 0-1.4-.23-1.96-.61V21c0 .55.45 1 1 1h16'
|
||||||
|
+ 'c.55 0 1-.45 1-1v-4.61c-.56.38-1.23.61-1.96.61-.92 0-1.79-.36-2.44-1.01zM18 9h-5V7h-2v2H6'
|
||||||
|
+ 'c-1.66 0-3 1.34-3 3v1.54c0 1.08.88 1.96 1.96 1.96.52 0 1.02-.2 1.38-.57l2.14-2.13 2.13 2.13'
|
||||||
|
+ 'c.74.74 2.03.74 2.77 0l2.14-2.13 2.13 2.13c.37.37.86.57 1.38.57 1.08 0 1.96-.88 1.96-1.96V12'
|
||||||
|
+ 'c.01-1.66-1.33-3-2.99-3z"/></svg>';
|
||||||
|
}
|
||||||
|
|
||||||
export function formatDate(d, opts = {}) {
|
export function formatDate(d, opts = {}) {
|
||||||
return d.toLocaleDateString('de', opts);
|
return d.toLocaleDateString('de', opts);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { isPast } from '../utils.js';
|
import { isPast, eventTitle, birthdayIconSvg } from '../utils.js';
|
||||||
import { t, getLang } from '../i18n.js';
|
import { t, getLang } from '../i18n.js';
|
||||||
|
|
||||||
export function renderAgenda(container, currentDate, events, onEventClick) {
|
export function renderAgenda(container, currentDate, events, onEventClick) {
|
||||||
@@ -45,7 +45,7 @@ export function renderAgenda(container, currentDate, events, onEventClick) {
|
|||||||
return `<div class="agenda-event ${pastCls}" data-id="${ev.id}" data-url="${escAttr(ev.url)}">
|
return `<div class="agenda-event ${pastCls}" data-id="${ev.id}" data-url="${escAttr(ev.url)}">
|
||||||
<div class="agenda-ev-color" style="background:${color}"></div>
|
<div class="agenda-ev-color" style="background:${color}"></div>
|
||||||
<div class="agenda-ev-info">
|
<div class="agenda-ev-info">
|
||||||
<div class="agenda-ev-title">${escHtml(ev.title)}</div>
|
<div class="agenda-ev-title">${ev.is_birthday ? birthdayIconSvg() : ''}${escHtml(eventTitle(ev))}</div>
|
||||||
<div class="agenda-ev-meta">${timeStr}${locHtml}</div>
|
<div class="agenda-ev-meta">${timeStr}${locHtml}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { isToday, isPast, isSameDay, dayOfWeek, weekStart, getISOWeekNumber } from '../utils.js';
|
import { isToday, isPast, isSameDay, dayOfWeek, weekStart, getISOWeekNumber, eventTitle, birthdayIconSvg } from '../utils.js';
|
||||||
import { t } from '../i18n.js';
|
import { t } from '../i18n.js';
|
||||||
|
|
||||||
const LANE_H = 20; // px per lane (event height 18px + 2px gap)
|
const LANE_H = 20; // px per lane (event height 18px + 2px gap)
|
||||||
@@ -110,14 +110,15 @@ export function renderMonth(container, currentDate, events, onDayClick, onEventC
|
|||||||
const pastCls = isPast(ev) ? 'past' : '';
|
const pastCls = isPast(ev) ? 'past' : '';
|
||||||
const cL = continuesLeft ? 'continues-left' : '';
|
const cL = continuesLeft ? 'continues-left' : '';
|
||||||
const cR = continuesRight ? 'continues-right' : '';
|
const cR = continuesRight ? 'continues-right' : '';
|
||||||
const titleEsc = escHtml(ev.title);
|
const titleEsc = escHtml(eventTitle(ev));
|
||||||
|
const icon = ev.is_birthday ? birthdayIconSvg() : '';
|
||||||
const labelHtml = ev.allDay
|
const labelHtml = ev.allDay
|
||||||
? titleEsc
|
? icon + titleEsc
|
||||||
: `<span class="month-event-time">${escHtml(fmtTime(new Date(ev.start)))}</span> ${titleEsc}`;
|
: `<span class="month-event-time">${escHtml(fmtTime(new Date(ev.start)))}</span> ${icon}${titleEsc}`;
|
||||||
eventsHtml += `<div class="month-span-event ${pastCls} ${cL} ${cR}"
|
eventsHtml += `<div class="month-span-event ${pastCls} ${cL} ${cR}"
|
||||||
data-id="${ev.id}" data-url="${escAttr(ev.url)}"
|
data-id="${ev.id}" data-url="${escAttr(ev.url)}"
|
||||||
style="left:${leftPct.toFixed(3)}%;width:${widthPct.toFixed(3)}%;top:${topPx}px;background:${color}"
|
style="left:${leftPct.toFixed(3)}%;width:${widthPct.toFixed(3)}%;top:${topPx}px;background:${color}"
|
||||||
title="${escAttr(ev.title)}">${labelHtml}</div>`;
|
title="${escAttr(eventTitle(ev))}">${labelHtml}</div>`;
|
||||||
});
|
});
|
||||||
|
|
||||||
// "+N more" per column
|
// "+N more" per column
|
||||||
@@ -292,7 +293,7 @@ function showOverflowPopup(anchor, date, events, onEventClick) {
|
|||||||
+ (continuesLeft ? ' continues-left' : '')
|
+ (continuesLeft ? ' continues-left' : '')
|
||||||
+ (continuesRight ? ' continues-right' : '');
|
+ (continuesRight ? ' continues-right' : '');
|
||||||
bar.style.background = color;
|
bar.style.background = color;
|
||||||
bar.textContent = ev.title;
|
bar.innerHTML = (ev.is_birthday ? birthdayIconSvg() : '') + escHtml(eventTitle(ev));
|
||||||
bar.addEventListener('click', e => {
|
bar.addEventListener('click', e => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
popup.remove();
|
popup.remove();
|
||||||
@@ -314,7 +315,7 @@ function showOverflowPopup(anchor, date, events, onEventClick) {
|
|||||||
|
|
||||||
const title = document.createElement('span');
|
const title = document.createElement('span');
|
||||||
title.className = 'mop-title';
|
title.className = 'mop-title';
|
||||||
title.textContent = ev.title;
|
title.innerHTML = (ev.is_birthday ? birthdayIconSvg() : '') + escHtml(eventTitle(ev));
|
||||||
|
|
||||||
row.append(dot, time, title);
|
row.append(dot, time, title);
|
||||||
row.addEventListener('click', e => {
|
row.addEventListener('click', e => {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { isToday, isPast, dayOfWeek } from '../utils.js';
|
import { isToday, isPast, dayOfWeek, eventTitle } from '../utils.js';
|
||||||
import { t } from '../i18n.js';
|
import { t } from '../i18n.js';
|
||||||
|
|
||||||
export function renderQuarter(container, currentDate, events, onDayClick, onEventClick, weekStartDay = 'monday') {
|
export function renderQuarter(container, currentDate, events, onDayClick, onEventClick, weekStartDay = 'monday') {
|
||||||
@@ -64,7 +64,7 @@ export function renderQuarter(container, currentDate, events, onDayClick, onEven
|
|||||||
const dots = cellEvs.slice(0, 3).map(ev => {
|
const dots = cellEvs.slice(0, 3).map(ev => {
|
||||||
const color = ev.color || ev.calendarColor || '#4285f4';
|
const color = ev.color || ev.calendarColor || '#4285f4';
|
||||||
const pastCls = isPast(ev) ? 'past' : '';
|
const pastCls = isPast(ev) ? 'past' : '';
|
||||||
return `<span class="qtr-dot ${pastCls}" style="background:${color}" title="${escAttr(ev.title)}" data-id="${ev.id}" data-url="${escAttr(ev.url || '')}"></span>`;
|
return `<span class="qtr-dot ${pastCls}" style="background:${color}" title="${escAttr(eventTitle(ev))}" data-id="${ev.id}" data-url="${escAttr(ev.url || '')}"></span>`;
|
||||||
}).join('');
|
}).join('');
|
||||||
const moreDot = cellEvs.length > 3
|
const moreDot = cellEvs.length > 3
|
||||||
? `<span class="qtr-dot-more">+${cellEvs.length - 3}</span>`
|
? `<span class="qtr-dot-more">+${cellEvs.length - 3}</span>`
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { isToday, isPast, dayOfWeek, weekStart, getISOWeekNumber } from '../utils.js';
|
import { isToday, isPast, dayOfWeek, weekStart, getISOWeekNumber, eventTitle, birthdayIconSvg } from '../utils.js';
|
||||||
import { t } from '../i18n.js';
|
import { t } from '../i18n.js';
|
||||||
|
|
||||||
export function renderWeek(container, currentDate, events, onSlotClick, onEventClick, isSingleDay = false, weekStartDay = 'monday', hourH = 60) {
|
export function renderWeek(container, currentDate, events, onSlotClick, onEventClick, isSingleDay = false, weekStartDay = 'monday', hourH = 60) {
|
||||||
@@ -77,11 +77,12 @@ export function renderWeek(container, currentDate, events, onSlotClick, onEventC
|
|||||||
const cL = evStart < firstDay ? 'continues-left' : '';
|
const cL = evStart < firstDay ? 'continues-left' : '';
|
||||||
const cR = (ev.allDay ? evEnd > lastDay : evEnd > lastDayMidnight) ? 'continues-right' : '';
|
const cR = (ev.allDay ? evEnd > lastDay : evEnd > lastDayMidnight) ? 'continues-right' : '';
|
||||||
const label = isMultiTimed && isSameDay(new Date(ev.start), days[colStart])
|
const label = isMultiTimed && isSameDay(new Date(ev.start), days[colStart])
|
||||||
? `${fmtTime(new Date(ev.start))} ${ev.title}`
|
? `${fmtTime(new Date(ev.start))} ${eventTitle(ev)}`
|
||||||
: ev.title;
|
: eventTitle(ev);
|
||||||
|
const icon = ev.is_birthday ? birthdayIconSvg() : '';
|
||||||
return `<div class="allday-span ${pastCls} ${multiCls} ${cL} ${cR}"
|
return `<div class="allday-span ${pastCls} ${multiCls} ${cL} ${cR}"
|
||||||
style="left:calc(${left.toFixed(2)}% + 1px);width:calc(${width.toFixed(2)}% - 2px);top:${top}px;background:${color};color:#fff"
|
style="left:calc(${left.toFixed(2)}% + 1px);width:calc(${width.toFixed(2)}% - 2px);top:${top}px;background:${color};color:#fff"
|
||||||
data-id="${ev.id}" data-url="${escAttr(ev.url)}" title="${escAttr(ev.title)}">${escHtml(label)}</div>`;
|
data-id="${ev.id}" data-url="${escAttr(ev.url)}" title="${escAttr(eventTitle(ev))}">${icon}${escHtml(label)}</div>`;
|
||||||
}).join('');
|
}).join('');
|
||||||
|
|
||||||
const alldayBgCols = days.map(day =>
|
const alldayBgCols = days.map(day =>
|
||||||
@@ -129,11 +130,12 @@ export function renderWeek(container, currentDate, events, onSlotClick, onEventC
|
|||||||
const isShort = height < 34;
|
const isShort = height < 34;
|
||||||
const shortCls = isShort ? 'short' : '';
|
const shortCls = isShort ? 'short' : '';
|
||||||
const locHtml = (!isShort && ev.location) ? `<div class="ev-loc">${escHtml(ev.location)}</div>` : '';
|
const locHtml = (!isShort && ev.location) ? `<div class="ev-loc">${escHtml(ev.location)}</div>` : '';
|
||||||
|
const icon = ev.is_birthday ? birthdayIconSvg() : '';
|
||||||
return `<div class="timed-event ${pastCls} ${shortCls}"
|
return `<div class="timed-event ${pastCls} ${shortCls}"
|
||||||
style="top:${top}px;height:${height}px;left:${left}%;width:${width}%;background:${color};color:#fff"
|
style="top:${top}px;height:${height}px;left:${left}%;width:${width}%;background:${color};color:#fff"
|
||||||
data-id="${ev.id}" data-url="${escAttr(ev.url)}" title="${escAttr(ev.title)}">
|
data-id="${ev.id}" data-url="${escAttr(ev.url)}" title="${escAttr(eventTitle(ev))}">
|
||||||
<div class="ev-time">${startStr}</div>
|
<div class="ev-time">${startStr}</div>
|
||||||
<div class="ev-title">${escHtml(ev.title)}</div>
|
<div class="ev-title">${icon}${escHtml(eventTitle(ev))}</div>
|
||||||
${locHtml}
|
${locHtml}
|
||||||
</div>`;
|
</div>`;
|
||||||
}).join('');
|
}).join('');
|
||||||
|
|||||||
Reference in New Issue
Block a user