Editorial redesign: event icons, timeline, person links
Reworks the newsletter as a family-chronicle layout: ivory paper
background, deep oxblood ink, aged-gold accents, EB Garamond
display with Georgia body fallback.
- Inline SVG event icons (sparkle for birth, dagger for death,
interlocked rings for marriage). Falls back silently in
Outlook desktop; modern Gmail / Apple / iOS / Outlook 365
render them.
- Right-side gold hairline timeline running through the date
column of every event row, with a filled dot per entry.
- Person names link to their webtrees Individual page via
Individual::url() (absolute URL through route() → BASE_URL),
including the avatar circles.
- German strings added for the new section kickers
("Family Chronicle", "Living kin who will celebrate this
fortnight.", "Marriages still intact.").
This commit is contained in:
+323
-157
@@ -24,53 +24,137 @@ use Illuminate\Support\Collection;
|
|||||||
* @var string $account_url
|
* @var string $account_url
|
||||||
*/
|
*/
|
||||||
|
|
||||||
$avatar_size = 48;
|
// ─── Aesthetic constants ────────────────────────────────────────────────
|
||||||
|
$palette = [
|
||||||
|
'paper' => '#f8f1e3', // warm ivory background
|
||||||
|
'ink' => '#3a2820', // deep oxblood-brown body text
|
||||||
|
'gold' => '#a17536', // aged-gold accents
|
||||||
|
'rule' => '#d4be91', // hairline rule
|
||||||
|
'mute' => '#7a6a5e', // muted secondary text
|
||||||
|
'shadow' => '#5a4a3e', // for darker symbols (death)
|
||||||
|
];
|
||||||
|
|
||||||
|
$serif_display = "'EB Garamond', Georgia, 'Times New Roman', serif";
|
||||||
|
$serif_body = "Georgia, 'Iowan Old Style', 'Palatino Linotype', serif";
|
||||||
|
|
||||||
|
$avatar_size = 56;
|
||||||
|
|
||||||
|
// ─── Helpers ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
$linked_name = static function (Individual $individual): string {
|
||||||
|
$name = strip_tags($individual->fullName());
|
||||||
|
$url = $individual->url();
|
||||||
|
$style = 'color:inherit;text-decoration:none;border-bottom:1px solid #c8a96a;';
|
||||||
|
|
||||||
|
return '<a href="' . e($url) . '" style="' . $style . '">' . e($name) . '</a>';
|
||||||
|
};
|
||||||
|
|
||||||
|
$record_label = static function (Fact $fact) use ($linked_name): string {
|
||||||
|
$record = $fact->record();
|
||||||
|
|
||||||
|
if ($record instanceof Individual) {
|
||||||
|
return $linked_name($record);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($record instanceof Family) {
|
||||||
|
$parts = [];
|
||||||
|
|
||||||
|
foreach ([$record->husband(), $record->wife()] as $spouse) {
|
||||||
|
if ($spouse instanceof Individual) {
|
||||||
|
$parts[] = $linked_name($spouse);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return implode(' & ', $parts);
|
||||||
|
}
|
||||||
|
|
||||||
|
return e($record->xref());
|
||||||
|
};
|
||||||
|
|
||||||
|
$event_date_display = static function (Fact $fact): string {
|
||||||
|
$date = $fact->date();
|
||||||
|
|
||||||
|
if (!$date instanceof Date || !$date->isOK()) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return strip_tags($date->display());
|
||||||
|
};
|
||||||
|
|
||||||
|
$event_kind = static function (Fact $fact): string {
|
||||||
|
$tag = $fact->tag();
|
||||||
|
$parts = explode(':', $tag);
|
||||||
|
|
||||||
|
return end($parts);
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Inline HTML for a single circular avatar.
|
* Compact inline SVG glyph for each event type. Rendered in modern
|
||||||
*
|
* webmail (Gmail web, Apple Mail, iOS, Outlook 365); Outlook desktop
|
||||||
* Renders an <img src="cid:..."> if the dispatch service was able to
|
* strips SVG silently — the textual event label still carries the
|
||||||
* resolve an image for the individual; otherwise renders a coloured
|
* meaning, so the email reads correctly without it.
|
||||||
* 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 {
|
$event_icon = static function (string $kind) use ($palette): string {
|
||||||
|
$svg_open = '<svg xmlns="http://www.w3.org/2000/svg" style="vertical-align:-3px;" ';
|
||||||
|
|
||||||
|
return match ($kind) {
|
||||||
|
// Eight-point star/sparkle for births.
|
||||||
|
'BIRT' => $svg_open . 'viewBox="0 0 24 24" width="18" height="18">'
|
||||||
|
. '<path fill="' . $palette['gold'] . '" d="M12 1 L13.4 9 L20.5 7.2 L15.4 12.5 L20.5 17.8 L13.4 16 L12 24 L10.6 16 L3.5 17.8 L8.6 12.5 L3.5 7.2 L10.6 9 Z"/>'
|
||||||
|
. '</svg>',
|
||||||
|
|
||||||
|
// Latin obelus/dagger — the typographic convention for "died" in
|
||||||
|
// obituaries since the 18th century. Restrained, narrow.
|
||||||
|
'DEAT' => $svg_open . 'viewBox="0 0 24 24" width="14" height="18">'
|
||||||
|
. '<rect x="10.5" y="2" width="3" height="20" fill="' . $palette['shadow'] . '"/>'
|
||||||
|
. '<rect x="5.5" y="7" width="13" height="3" fill="' . $palette['shadow'] . '"/>'
|
||||||
|
. '</svg>',
|
||||||
|
|
||||||
|
// Two interlocking rings — universal heraldic mark of marriage.
|
||||||
|
'MARR' => $svg_open . 'viewBox="0 0 36 22" width="28" height="18">'
|
||||||
|
. '<circle cx="13" cy="11" r="8" fill="none" stroke="' . $palette['gold'] . '" stroke-width="1.8"/>'
|
||||||
|
. '<circle cx="23" cy="11" r="8" fill="none" stroke="' . $palette['gold'] . '" stroke-width="1.8"/>'
|
||||||
|
. '</svg>',
|
||||||
|
|
||||||
|
default => '',
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A single circular avatar — either the embedded image, or a coloured
|
||||||
|
* disc with the person's initials. Wraps in an <a> linking to the
|
||||||
|
* individual's webtrees page when available.
|
||||||
|
*/
|
||||||
|
$avatar = static function (Individual|null $individual) use ($avatar_cids, $avatar_size, $palette): string {
|
||||||
if (!$individual instanceof Individual) {
|
if (!$individual instanceof Individual) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
$alt = e(strip_tags($individual->fullName()));
|
$alt = e(strip_tags($individual->fullName()));
|
||||||
|
|
||||||
|
$shadow = 'box-shadow:0 0 0 1px ' . $palette['rule'] . ',0 2px 6px rgba(58,40,32,0.18);';
|
||||||
|
|
||||||
if (isset($avatar_cids[$individual->xref()])) {
|
if (isset($avatar_cids[$individual->xref()])) {
|
||||||
$cid = $avatar_cids[$individual->xref()];
|
$cid = $avatar_cids[$individual->xref()];
|
||||||
|
$inner = '<img src="cid:' . e($cid) . '" alt="' . $alt . '"'
|
||||||
return '<img src="cid:' . e($cid) . '" alt="' . $alt . '" width="' . $avatar_size . '" height="' . $avatar_size . '"'
|
. ' width="' . $avatar_size . '" height="' . $avatar_size . '"'
|
||||||
. ' style="border-radius:50%;object-fit:cover;vertical-align:middle;border:1px solid #ccc;">';
|
. ' style="border-radius:50%;object-fit:cover;display:block;' . $shadow . '">';
|
||||||
}
|
} else {
|
||||||
|
|
||||||
// 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;
|
$hue = hexdec(substr(md5($individual->xref()), 0, 2)) * 360 / 255;
|
||||||
$first = strip_tags($individual->getAllNames()[0]['givn'] ?? $individual->xref());
|
$first = strip_tags($individual->getAllNames()[0]['givn'] ?? $individual->xref());
|
||||||
$last = strip_tags($individual->getAllNames()[0]['surn'] ?? '');
|
$last = strip_tags($individual->getAllNames()[0]['surn'] ?? '');
|
||||||
$i1 = mb_substr($first, 0, 1);
|
$initials = e(mb_strtoupper(mb_substr($first, 0, 1) . mb_substr($last, 0, 1)));
|
||||||
$i2 = mb_substr($last, 0, 1);
|
|
||||||
$initials = e(mb_strtoupper($i1 . $i2));
|
|
||||||
|
|
||||||
return '<span aria-label="' . $alt . '"'
|
$inner = '<span aria-label="' . $alt . '" style="'
|
||||||
. ' style="display:inline-block;width:' . $avatar_size . 'px;height:' . $avatar_size . 'px;'
|
. 'display:block;width:' . $avatar_size . 'px;height:' . $avatar_size . 'px;'
|
||||||
. 'border-radius:50%;background:hsl(' . (int) $hue . ',45%,60%);color:#fff;'
|
. 'border-radius:50%;background:hsl(' . (int) $hue . ',38%,55%);color:#fff;'
|
||||||
. 'font:600 18px/' . $avatar_size . 'px Helvetica,Arial,sans-serif;text-align:center;'
|
. "font:600 20px/{$avatar_size}px Georgia,serif;text-align:center;"
|
||||||
. 'vertical-align:middle;letter-spacing:0.5px;">' . $initials . '</span>';
|
. 'letter-spacing:0.5px;' . $shadow . '">' . $initials . '</span>';
|
||||||
|
}
|
||||||
|
|
||||||
|
return '<a href="' . e($individual->url()) . '" style="text-decoration:none;">' . $inner . '</a>';
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* 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_avatars = static function (Fact $fact) use ($avatar): string {
|
||||||
$record = $fact->record();
|
$record = $fact->record();
|
||||||
|
|
||||||
@@ -87,55 +171,41 @@ $record_avatars = static function (Fact $fact) use ($avatar): string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Slight negative margin so the two circles overlap a touch —
|
// Two circles overlapping by a few px reads as "couple" without
|
||||||
// visually communicates "couple" without needing extra glue.
|
// needing additional glue characters.
|
||||||
return '<span style="display:inline-block;white-space:nowrap;">'
|
return '<table cellpadding="0" cellspacing="0" border="0"><tr>'
|
||||||
. implode('<span style="display:inline-block;width:6px;"></span>', $parts)
|
. '<td>' . ($parts[0] ?? '') . '</td>'
|
||||||
. '</span>';
|
. '<td style="padding-left:0;width:0;"></td>'
|
||||||
|
. (isset($parts[1])
|
||||||
|
? '<td style="padding-left:8px;">' . $parts[1] . '</td>'
|
||||||
|
: '')
|
||||||
|
. '</tr></table>';
|
||||||
}
|
}
|
||||||
|
|
||||||
return '';
|
return '';
|
||||||
};
|
};
|
||||||
|
|
||||||
$record_label = static function (Fact $fact): string {
|
// Locale-aware ordinal: English uses st/nd/rd/th; German just appends ".".
|
||||||
$record = $fact->record();
|
$ordinal = static function (int $n): string {
|
||||||
|
if (str_starts_with(I18N::languageTag(), 'de')) {
|
||||||
if ($record instanceof Individual) {
|
return $n . '.';
|
||||||
return strip_tags($record->fullName());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($record instanceof Family) {
|
$abs = abs($n);
|
||||||
$husband = $record->husband();
|
$mod100 = $abs % 100;
|
||||||
$wife = $record->wife();
|
|
||||||
$names = array_filter([
|
|
||||||
$husband !== null ? strip_tags($husband->fullName()) : '',
|
|
||||||
$wife !== null ? strip_tags($wife->fullName()) : '',
|
|
||||||
]);
|
|
||||||
|
|
||||||
return implode(' & ', $names);
|
if ($mod100 >= 11 && $mod100 <= 13) {
|
||||||
|
return $n . 'th';
|
||||||
}
|
}
|
||||||
|
|
||||||
return $record->xref();
|
return $n . match ($abs % 10) {
|
||||||
|
1 => 'st',
|
||||||
|
2 => 'nd',
|
||||||
|
3 => 'rd',
|
||||||
|
default => 'th',
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
$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 {
|
$upcoming_age = static function (Fact $fact): int {
|
||||||
static $gregorian = null;
|
static $gregorian = null;
|
||||||
$gregorian ??= new \Fisharebest\ExtCalendar\GregorianCalendar();
|
$gregorian ??= new \Fisharebest\ExtCalendar\GregorianCalendar();
|
||||||
@@ -158,131 +228,227 @@ $upcoming_age = static function (Fact $fact): int {
|
|||||||
return max(0, $upcoming_year - $event_year);
|
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 {
|
$birthday_label = static function (int $age) use ($ordinal): string {
|
||||||
if ($age <= 0) {
|
return $age > 0
|
||||||
return I18N::translate('Birthday');
|
? I18N::translate('%s birthday', $ordinal($age))
|
||||||
}
|
: I18N::translate('Birthday');
|
||||||
|
|
||||||
return I18N::translate('%s birthday', $ordinal($age));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
$anniversary_label = static function (int $age) use ($ordinal): string {
|
$anniversary_label = static function (int $age) use ($ordinal): string {
|
||||||
if ($age <= 0) {
|
return $age > 0
|
||||||
return I18N::translate('Wedding anniversary');
|
? I18N::translate('%s wedding anniversary', $ordinal($age))
|
||||||
|
: I18N::translate('Wedding anniversary');
|
||||||
|
};
|
||||||
|
|
||||||
|
// Styles reused across the event lists.
|
||||||
|
$row_padding = '20px 0';
|
||||||
|
$divider_style = 'border-bottom:1px solid ' . $palette['rule'] . ';';
|
||||||
|
|
||||||
|
$section_title_style = 'margin:48px 0 8px;'
|
||||||
|
. 'font-family:' . $serif_display . ';'
|
||||||
|
. 'font-weight:500;font-size:28px;line-height:1.1;'
|
||||||
|
. 'color:' . $palette['ink'] . ';'
|
||||||
|
. 'letter-spacing:0.005em;';
|
||||||
|
|
||||||
|
$section_kicker_style = 'margin:0 0 22px;'
|
||||||
|
. 'font-family:' . $serif_display . ';'
|
||||||
|
. 'font-style:italic;font-size:15px;'
|
||||||
|
. 'color:' . $palette['mute'] . ';'
|
||||||
|
. 'letter-spacing:0.02em;';
|
||||||
|
|
||||||
|
$timeline_cell_style = 'width:170px;vertical-align:top;'
|
||||||
|
. 'padding:24px 0 24px 28px;'
|
||||||
|
. 'border-left:1px solid ' . $palette['rule'] . ';'
|
||||||
|
. 'font-family:' . $serif_body . ';'
|
||||||
|
. 'color:' . $palette['mute'] . ';'
|
||||||
|
. 'font-size:12px;letter-spacing:0.12em;text-transform:uppercase;'
|
||||||
|
. 'white-space:nowrap;';
|
||||||
|
|
||||||
|
$timeline_dot_style = 'display:inline-block;'
|
||||||
|
. 'width:11px;height:11px;background:' . $palette['gold'] . ';'
|
||||||
|
. 'border-radius:50%;'
|
||||||
|
. 'margin-left:-34px;margin-right:14px;vertical-align:middle;';
|
||||||
|
|
||||||
|
$content_cell_style = 'vertical-align:middle;'
|
||||||
|
. 'padding:24px 24px 24px 18px;'
|
||||||
|
. 'font-family:' . $serif_body . ';font-size:16px;line-height:1.4;'
|
||||||
|
. 'color:' . $palette['ink'] . ';';
|
||||||
|
|
||||||
|
$avatar_cell_style = 'width:64px;vertical-align:middle;padding:18px 0;';
|
||||||
|
|
||||||
|
// Renders one event row in the consistent 3-column layout shared by all
|
||||||
|
// sections (avatar | content | timeline + date).
|
||||||
|
/**
|
||||||
|
* Locale-aware long-form date for the masthead. German edition reads
|
||||||
|
* "15. Mai 2026" rather than the English "May 15, 2026".
|
||||||
|
*/
|
||||||
|
$masthead_date = static function (int $timestamp): string {
|
||||||
|
if (str_starts_with(I18N::languageTag(), 'de')) {
|
||||||
|
$months = [
|
||||||
|
1 => 'Januar', 'Februar', 'März', 'April', 'Mai', 'Juni',
|
||||||
|
'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember',
|
||||||
|
];
|
||||||
|
|
||||||
|
return (int) date('j', $timestamp) . '. '
|
||||||
|
. $months[(int) date('n', $timestamp)] . ' '
|
||||||
|
. date('Y', $timestamp);
|
||||||
}
|
}
|
||||||
|
|
||||||
return I18N::translate('%s wedding anniversary', $ordinal($age));
|
return date('F j, Y', $timestamp);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
$event_row = static function (Fact $fact, string $body_html)
|
||||||
|
use (
|
||||||
|
$record_avatars,
|
||||||
|
$event_date_display,
|
||||||
|
$avatar_cell_style,
|
||||||
|
$content_cell_style,
|
||||||
|
$timeline_cell_style,
|
||||||
|
$timeline_dot_style,
|
||||||
|
$divider_style,
|
||||||
|
): string {
|
||||||
|
return '<tr style="' . $divider_style . '">'
|
||||||
|
. '<td style="' . $avatar_cell_style . '">' . $record_avatars($fact) . '</td>'
|
||||||
|
. '<td style="' . $content_cell_style . '">' . $body_html . '</td>'
|
||||||
|
. '<td style="' . $timeline_cell_style . '">'
|
||||||
|
. '<span style="' . $timeline_dot_style . '"></span>'
|
||||||
|
. e($event_date_display($fact))
|
||||||
|
. '</td>'
|
||||||
|
. '</tr>';
|
||||||
|
};
|
||||||
|
|
||||||
?><!doctype html>
|
?><!doctype html>
|
||||||
<html lang="<?= e(I18N::languageTag()) ?>">
|
<html lang="<?= e(I18N::languageTag()) ?>">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title><?= e(I18N::translate('Family newsletter — %s', date('F j, Y', $generated_at))) ?></title>
|
<title><?= e(I18N::translate('Family newsletter — %s', date('F j, Y', $generated_at))) ?></title>
|
||||||
|
<style>
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=EB+Garamond:ital,wght@0,400;0,500;0,600;1,400&display=swap');
|
||||||
|
body { margin: 0; padding: 0; background: <?= $palette['paper'] ?>; }
|
||||||
|
a:hover { opacity: 0.75; }
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body style="font-family: Helvetica, Arial, sans-serif; color: #222; max-width: 640px; margin: 0 auto;">
|
<body style="margin:0;padding:0;background:<?= $palette['paper'] ?>;color:<?= $palette['ink'] ?>;">
|
||||||
|
|
||||||
<h1 style="border-bottom: 2px solid #888; padding-bottom: 0.3rem;">
|
<table role="presentation" cellpadding="0" cellspacing="0" border="0"
|
||||||
|
style="width:100%;background:<?= $palette['paper'] ?>;">
|
||||||
|
<tr>
|
||||||
|
<td align="center" style="padding:32px 16px;">
|
||||||
|
|
||||||
|
<table role="presentation" cellpadding="0" cellspacing="0" border="0"
|
||||||
|
style="width:100%;max-width:720px;background:<?= $palette['paper'] ?>;">
|
||||||
|
|
||||||
|
<!-- Masthead ─────────────────────────────────────────── -->
|
||||||
|
<tr>
|
||||||
|
<td style="padding:24px 8px 8px;text-align:center;">
|
||||||
|
<div style="font-family:<?= $serif_display ?>;font-style:italic;font-size:13px;letter-spacing:0.4em;text-transform:uppercase;color:<?= $palette['gold'] ?>;">
|
||||||
|
<?= e(I18N::translate('Family Chronicle')) ?>
|
||||||
|
</div>
|
||||||
|
<h1 style="margin:14px 0 6px;font-family:<?= $serif_display ?>;font-weight:500;font-size:46px;line-height:1.05;letter-spacing:-0.005em;color:<?= $palette['ink'] ?>;">
|
||||||
<?= e($tree->title()) ?>
|
<?= e($tree->title()) ?>
|
||||||
</h1>
|
</h1>
|
||||||
|
<div style="font-family:<?= $serif_body ?>;font-size:14px;color:<?= $palette['mute'] ?>;letter-spacing:0.05em;">
|
||||||
|
<?= e($masthead_date($generated_at)) ?>
|
||||||
|
</div>
|
||||||
|
<div style="margin:22px auto 8px;width:80px;border-top:1px solid <?= $palette['gold'] ?>;"></div>
|
||||||
|
<div style="font-family:<?= $serif_display ?>;font-size:18px;color:<?= $palette['gold'] ?>;letter-spacing:0.3em;">❦</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
<p style="color: #555;">
|
<!-- Intro ────────────────────────────────────────────── -->
|
||||||
|
<tr>
|
||||||
|
<td style="padding:14px 16px 4px;text-align:center;font-family:<?= $serif_body ?>;font-style:italic;font-size:16px;color:<?= $palette['mute'] ?>;">
|
||||||
<?= e(I18N::translate('Events in the next %d days.', $lookahead_days)) ?>
|
<?= e(I18N::translate('Events in the next %d days.', $lookahead_days)) ?>
|
||||||
</p>
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
<?php
|
<?php if (!$birthdays->isEmpty()) : ?>
|
||||||
// Inline list style: drop bullets, add vertical spacing so the avatars
|
<tr><td style="padding:0 16px;">
|
||||||
// don't crash into each other.
|
<h2 style="<?= $section_title_style ?>"><?= e(I18N::translate('Upcoming birthdays')) ?></h2>
|
||||||
$list_style = 'list-style:none;padding:0;margin:0.5rem 0 1.5rem;';
|
<p style="<?= $section_kicker_style ?>">
|
||||||
$item_style = 'display:flex;align-items:center;gap:0.75rem;padding:0.4rem 0;border-bottom:1px solid #eee;';
|
<?= e(I18N::translate('Living kin who will celebrate this fortnight.')) ?>
|
||||||
?>
|
</p>
|
||||||
|
<table role="presentation" cellpadding="0" cellspacing="0" border="0" style="width:100%;border-collapse:collapse;">
|
||||||
<?php if (!$birthdays->isEmpty()) : ?>
|
|
||||||
<h2 style="color: #336;"><?= e(I18N::translate('Upcoming birthdays')) ?></h2>
|
|
||||||
<ul style="<?= $list_style ?>">
|
|
||||||
<?php foreach ($birthdays as $fact) : ?>
|
<?php foreach ($birthdays as $fact) : ?>
|
||||||
<?php $age = $upcoming_age($fact); ?>
|
<?php
|
||||||
<li style="<?= $item_style ?>">
|
$age = $upcoming_age($fact);
|
||||||
<?= $record_avatars($fact) ?>
|
$body = $event_icon('BIRT')
|
||||||
<span>
|
. '<span style="margin-left:10px;">'
|
||||||
<strong><?= e($record_label($fact)) ?></strong>
|
. '<strong style="font-weight:600;">' . $record_label($fact) . '</strong>'
|
||||||
— <?= e($birthday_label($age)) ?>
|
. ' — <span style="color:' . $palette['mute'] . ';">'
|
||||||
<span style="color: #666;">(<?= e($event_date($fact)) ?>)</span>
|
. e($birthday_label($age)) . '</span>'
|
||||||
</span>
|
. '</span>';
|
||||||
</li>
|
echo $event_row($fact, $body);
|
||||||
|
?>
|
||||||
<?php endforeach ?>
|
<?php endforeach ?>
|
||||||
</ul>
|
</table>
|
||||||
<?php endif ?>
|
</td></tr>
|
||||||
|
<?php endif ?>
|
||||||
|
|
||||||
<?php if ($include_anniversaries && $anniversaries !== null && !$anniversaries->isEmpty()) : ?>
|
<?php if ($include_anniversaries && $anniversaries !== null && !$anniversaries->isEmpty()) : ?>
|
||||||
<h2 style="color: #336;"><?= e(I18N::translate('Upcoming marriage anniversaries')) ?></h2>
|
<tr><td style="padding:0 16px;">
|
||||||
<ul style="<?= $list_style ?>">
|
<h2 style="<?= $section_title_style ?>"><?= e(I18N::translate('Upcoming marriage anniversaries')) ?></h2>
|
||||||
|
<p style="<?= $section_kicker_style ?>">
|
||||||
|
<?= e(I18N::translate('Marriages still intact.')) ?>
|
||||||
|
</p>
|
||||||
|
<table role="presentation" cellpadding="0" cellspacing="0" border="0" style="width:100%;border-collapse:collapse;">
|
||||||
<?php foreach ($anniversaries as $fact) : ?>
|
<?php foreach ($anniversaries as $fact) : ?>
|
||||||
<?php $age = $upcoming_age($fact); ?>
|
<?php
|
||||||
<li style="<?= $item_style ?>">
|
$age = $upcoming_age($fact);
|
||||||
<?= $record_avatars($fact) ?>
|
$body = $event_icon('MARR')
|
||||||
<span>
|
. '<span style="margin-left:10px;">'
|
||||||
<strong><?= e($record_label($fact)) ?></strong>
|
. '<strong style="font-weight:600;">' . $record_label($fact) . '</strong>'
|
||||||
— <?= e($anniversary_label($age)) ?>
|
. ' — <span style="color:' . $palette['mute'] . ';">'
|
||||||
<span style="color: #666;">(<?= e($event_date($fact)) ?>)</span>
|
. e($anniversary_label($age)) . '</span>'
|
||||||
</span>
|
. '</span>';
|
||||||
</li>
|
echo $event_row($fact, $body);
|
||||||
|
?>
|
||||||
<?php endforeach ?>
|
<?php endforeach ?>
|
||||||
</ul>
|
</table>
|
||||||
<?php endif ?>
|
</td></tr>
|
||||||
|
<?php endif ?>
|
||||||
|
|
||||||
<?php if ($include_historical && $historical !== null && !$historical->isEmpty()) : ?>
|
<?php if ($include_historical && $historical !== null && !$historical->isEmpty()) : ?>
|
||||||
<h2 style="color: #663;"><?= e(I18N::translate('On this month in history')) ?></h2>
|
<tr><td style="padding:0 16px;">
|
||||||
<p style="color: #666;">
|
<h2 style="<?= $section_title_style ?>"><?= e(I18N::translate('On this month in history')) ?></h2>
|
||||||
|
<p style="<?= $section_kicker_style ?>">
|
||||||
<?= e(I18N::translate('Events in the next %d days for people who have passed away.', $historical_lookahead)) ?>
|
<?= e(I18N::translate('Events in the next %d days for people who have passed away.', $historical_lookahead)) ?>
|
||||||
</p>
|
</p>
|
||||||
<ul style="<?= $list_style ?>">
|
<table role="presentation" cellpadding="0" cellspacing="0" border="0" style="width:100%;border-collapse:collapse;">
|
||||||
<?php foreach ($historical as $fact) : ?>
|
<?php foreach ($historical as $fact) : ?>
|
||||||
<li style="<?= $item_style ?>">
|
<?php
|
||||||
<?= $record_avatars($fact) ?>
|
$kind = $event_kind($fact);
|
||||||
<span>
|
$body = $event_icon($kind)
|
||||||
<strong><?= e($record_label($fact)) ?></strong>
|
. '<span style="margin-left:10px;">'
|
||||||
— <?= e($fact->label()) ?>: <?= e($event_date($fact)) ?>
|
. '<strong style="font-weight:600;">' . $record_label($fact) . '</strong>'
|
||||||
</span>
|
. ' — <span style="color:' . $palette['mute'] . ';">'
|
||||||
</li>
|
. e($fact->label()) . '</span>'
|
||||||
|
. '</span>';
|
||||||
|
echo $event_row($fact, $body);
|
||||||
|
?>
|
||||||
<?php endforeach ?>
|
<?php endforeach ?>
|
||||||
</ul>
|
</table>
|
||||||
<?php endif ?>
|
</td></tr>
|
||||||
|
<?php endif ?>
|
||||||
|
|
||||||
<hr style="margin-top: 2rem; border: 0; border-top: 1px solid #ccc;">
|
<!-- Footer ─────────────────────────────────────────── -->
|
||||||
<p style="color: #888; font-size: 0.85rem;">
|
<tr><td style="padding:48px 16px 24px;text-align:center;">
|
||||||
|
<div style="font-family:<?= $serif_display ?>;font-size:18px;color:<?= $palette['gold'] ?>;letter-spacing:0.3em;">❦</div>
|
||||||
|
<p style="margin:22px auto 0;max-width:520px;font-family:<?= $serif_body ?>;font-size:13px;line-height:1.6;color:<?= $palette['mute'] ?>;font-style:italic;">
|
||||||
<?= e(I18N::translate('You are receiving this email because you subscribed to the %s newsletter.', $tree->title())) ?>
|
<?= e(I18N::translate('You are receiving this email because you subscribed to the %s newsletter.', $tree->title())) ?>
|
||||||
<br>
|
<br>
|
||||||
<?= I18N::translate(
|
<?= I18N::translate(
|
||||||
'To change or cancel your subscription, edit the “Newsletter subscription” section on your %s page.',
|
'To change or cancel your subscription, edit the “Newsletter subscription” section on your %s page.',
|
||||||
'<a href="' . e($account_url) . '" style="color: #336;">' . e(I18N::translate('My account')) . '</a>',
|
'<a href="' . e($account_url) . '" style="color:' . $palette['gold'] . ';text-decoration:none;border-bottom:1px solid ' . $palette['gold'] . ';">' . e(I18N::translate('My account')) . '</a>',
|
||||||
) ?>
|
) ?>
|
||||||
</p>
|
</p>
|
||||||
|
</td></tr>
|
||||||
|
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -118,6 +118,10 @@ class Module extends AbstractModule implements ModuleCustomInterface, ModuleConf
|
|||||||
'Cron token' => 'Cron-Token',
|
'Cron token' => 'Cron-Token',
|
||||||
'Regenerate token' => 'Token neu generieren',
|
'Regenerate token' => 'Token neu generieren',
|
||||||
'Your subscription has been updated.' => 'Ihr Abonnement wurde aktualisiert.',
|
'Your subscription has been updated.' => 'Ihr Abonnement wurde aktualisiert.',
|
||||||
|
'Family Chronicle' => 'Familienchronik',
|
||||||
|
'Living kin who will celebrate this fortnight.'
|
||||||
|
=> 'Lebende Verwandte, die in den nächsten zwei Wochen feiern.',
|
||||||
|
'Marriages still intact.' => 'Noch bestehende Ehen.',
|
||||||
'%s birthday' => '%s Geburtstag',
|
'%s birthday' => '%s Geburtstag',
|
||||||
'%s wedding anniversary' => '%s Hochzeitstag',
|
'%s wedding anniversary' => '%s Hochzeitstag',
|
||||||
'Birthday' => 'Geburtstag',
|
'Birthday' => 'Geburtstag',
|
||||||
|
|||||||
Reference in New Issue
Block a user