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:
Scarriffle
2026-07-12 22:10:52 +02:00
parent ca09538971
commit eb0684b99c
9 changed files with 250 additions and 22 deletions

View File

@@ -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;

View File

@@ -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">&times;</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">

View File

@@ -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 = '';

View File

@@ -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',

View File

@@ -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);
} }

View File

@@ -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>`;

View File

@@ -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 => {

View File

@@ -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>`

View File

@@ -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('');