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

@@ -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';
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 cR = (ev.allDay ? evEnd > lastDay : evEnd > lastDayMidnight) ? 'continues-right' : '';
const label = isMultiTimed && isSameDay(new Date(ev.start), days[colStart])
? `${fmtTime(new Date(ev.start))} ${ev.title}`
: ev.title;
? `${fmtTime(new Date(ev.start))} ${eventTitle(ev)}`
: eventTitle(ev);
const icon = ev.is_birthday ? birthdayIconSvg() : '';
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"
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('');
const alldayBgCols = days.map(day =>
@@ -129,11 +130,12 @@ export function renderWeek(container, currentDate, events, onSlotClick, onEventC
const isShort = height < 34;
const shortCls = isShort ? 'short' : '';
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}"
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-title">${escHtml(ev.title)}</div>
<div class="ev-title">${icon}${escHtml(eventTitle(ev))}</div>
${locHtml}
</div>`;
}).join('');