$birthdays * @var Collection|null $anniversaries * @var Collection|null $historical * @var bool $include_anniversaries * @var bool $include_historical * @var int $lookahead_days * @var int $historical_lookahead * @var int $generated_at * @var array $avatar_cids xref => CID name * @var string $account_url */ $avatar_size = 48; /** * Inline HTML for a single circular avatar. * * Renders an if the dispatch service was able to * resolve an image for the individual; otherwise renders a coloured * circle with the person's initials. The placeholder is intentionally * CSS-only — inline SVG and data: URIs are unreliable in Outlook / * some webmail clients. */ $avatar = static function (Individual|null $individual) use ($avatar_cids, $avatar_size): string { if (!$individual instanceof Individual) { return ''; } $alt = e(strip_tags($individual->fullName())); if (isset($avatar_cids[$individual->xref()])) { $cid = $avatar_cids[$individual->xref()]; return '' . $alt . ''; } // CSS-only fallback: coloured circle with initials. Hash the xref // into a stable hue so each person keeps the same colour across // newsletters. $hue = hexdec(substr(md5($individual->xref()), 0, 2)) * 360 / 255; $first = strip_tags($individual->getAllNames()[0]['givn'] ?? $individual->xref()); $last = strip_tags($individual->getAllNames()[0]['surn'] ?? ''); $i1 = mb_substr($first, 0, 1); $i2 = mb_substr($last, 0, 1); $initials = e(mb_strtoupper($i1 . $i2)); return '' . $initials . ''; }; /** * HTML for the avatar(s) attached to a Fact's primary record: * a single circle for Individual facts, side-by-side circles for * Family facts (anniversaries). */ $record_avatars = static function (Fact $fact) use ($avatar): string { $record = $fact->record(); if ($record instanceof Individual) { return $avatar($record); } if ($record instanceof Family) { $parts = []; foreach ([$record->husband(), $record->wife()] as $spouse) { if ($spouse instanceof Individual) { $parts[] = $avatar($spouse); } } // Slight negative margin so the two circles overlap a touch — // visually communicates "couple" without needing extra glue. return '' . implode('', $parts) . ''; } return ''; }; $record_label = static function (Fact $fact): string { $record = $fact->record(); if ($record instanceof Individual) { return strip_tags($record->fullName()); } if ($record instanceof Family) { $husband = $record->husband(); $wife = $record->wife(); $names = array_filter([ $husband !== null ? strip_tags($husband->fullName()) : '', $wife !== null ? strip_tags($wife->fullName()) : '', ]); return implode(' & ', $names); } return $record->xref(); }; $event_date = static function (Fact $fact): string { $date = $fact->date(); if (!$date instanceof Date || !$date->isOK()) { return ''; } return strip_tags($date->display()); }; /** * Age the person/couple actually turns on the upcoming anniversary, not * their current age. We use the fact's own year (which on an anniversary * Fact is the year of the original event — birth or marriage) and the * year of the upcoming Julian day stored on the Fact ($fact->jd) so the * calculation handles people whose birthday falls before vs. after today * uniformly. */ $upcoming_age = static function (Fact $fact): int { static $gregorian = null; $gregorian ??= new \Fisharebest\ExtCalendar\GregorianCalendar(); $date = $fact->date(); if (!$date->isOK()) { return 0; } $event_year = $date->gregorianYear(); $upcoming_jd = $fact->jd ?? 0; if ($upcoming_jd > 0) { [$upcoming_year] = $gregorian->jdToYmd($upcoming_jd); } else { $upcoming_year = (int) date('Y'); } return max(0, $upcoming_year - $event_year); }; /** * Locale-aware ordinal. English uses st/nd/rd/th suffixes; German (and * most other European languages we currently support) just appends a * period to the digits. */ $ordinal = static function (int $n): string { if (str_starts_with(I18N::languageTag(), 'de')) { return $n . '.'; } $abs = abs($n); $mod100 = $abs % 100; if ($mod100 >= 11 && $mod100 <= 13) { return $n . 'th'; } return $n . match ($abs % 10) { 1 => 'st', 2 => 'nd', 3 => 'rd', default => 'th', }; }; $birthday_label = static function (int $age) use ($ordinal): string { if ($age <= 0) { return I18N::translate('Birthday'); } return I18N::translate('%s birthday', $ordinal($age)); }; $anniversary_label = static function (int $age) use ($ordinal): string { if ($age <= 0) { return I18N::translate('Wedding anniversary'); } return I18N::translate('%s wedding anniversary', $ordinal($age)); }; ?> <?= e(I18N::translate('Family newsletter — %s', date('F j, Y', $generated_at))) ?>

title()) ?>

isEmpty()) : ?>

  • ()
isEmpty()) : ?>

  • ()
isEmpty()) : ?>

  • label()) ?>:

title())) ?>
' . e(I18N::translate('My account')) . '', ) ?>