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

@@ -16,6 +16,26 @@ export function isPast(ev) {
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 = {}) {
return d.toLocaleDateString('de', opts);
}