582 lines
24 KiB
PHTML
582 lines
24 KiB
PHTML
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use Fisharebest\Webtrees\Date;
|
|
use Fisharebest\Webtrees\Fact;
|
|
use Fisharebest\Webtrees\Family;
|
|
use Fisharebest\Webtrees\I18N;
|
|
use Fisharebest\Webtrees\Individual;
|
|
use Fisharebest\Webtrees\Tree;
|
|
use Illuminate\Support\Collection;
|
|
|
|
/**
|
|
* Upcoming-events block for the tree home page.
|
|
*
|
|
* Renders the same editorial card-and-timeline visualisation as the
|
|
* newsletter email, with two adaptations for web context:
|
|
* - avatars resolve to https URLs (`$avatar_srcs`) rather than the
|
|
* email's `cid:` MIME-part references;
|
|
* - everyone is "detailed" — there's no per-recipient kin-distance
|
|
* filter, since the block is rendered once for the whole tree.
|
|
*
|
|
* Keep this file structurally close to email.phtml so the two stay
|
|
* visually identical when one is tweaked.
|
|
*
|
|
* @var Tree $tree
|
|
* @var Collection<int,Fact> $birthdays
|
|
* @var Collection<int,Fact>|null $anniversaries
|
|
* @var Collection<int,Fact>|null $historical
|
|
* @var bool $include_anniversaries
|
|
* @var int $window_days
|
|
* @var array<string,string> $avatar_srcs xref => https URL of the highlight image
|
|
* @var array<string,string> $relationships xref => "your mother" (per-viewer when signed-in + linked)
|
|
*/
|
|
|
|
// ─── Palette ────────────────────────────────────────────────────────────
|
|
//
|
|
// Bind to BockenTheme CSS custom properties instead of fixed hex values
|
|
// so the block automatically tracks the user's light/dark preference
|
|
// (and any future theme tweak) without us re-implementing dark-mode
|
|
// overrides. Where the theme has no semantic token for a particular
|
|
// role — the rail or the dot, for instance — we lean on `--color-border`
|
|
// (subtle in both modes) and `--color-link` (the Nord blue swap-pair).
|
|
$palette = [
|
|
'bg' => 'var(--color-bg-primary)',
|
|
'surface' => 'var(--color-bg-secondary)',
|
|
'border' => 'var(--color-border)',
|
|
'ink' => 'var(--color-text-primary)',
|
|
'ink2' => 'var(--color-text-secondary)',
|
|
'ink3' => 'var(--color-text-tertiary)',
|
|
'mute' => 'var(--color-text-muted)',
|
|
'link' => 'var(--color-link)',
|
|
'link_hov' => 'var(--color-link-hover)',
|
|
'accent' => 'var(--color-accent)',
|
|
'birth' => 'var(--color-link)',
|
|
'death' => 'var(--color-text-secondary)',
|
|
'marr' => 'var(--nord15)',
|
|
'rail' => 'var(--color-border)',
|
|
'dot' => 'var(--color-link)',
|
|
];
|
|
|
|
$font_stack = "inherit";
|
|
$avatar_size = 56;
|
|
|
|
$relationships = $relationships ?? [];
|
|
$avatar_srcs = $avatar_srcs ?? [];
|
|
|
|
// ─── Helpers ────────────────────────────────────────────────────────────
|
|
|
|
$linked_name = static function (Individual $individual) use ($palette): string {
|
|
$name = strip_tags($individual->fullName());
|
|
$url = $individual->url();
|
|
// No bottom border — link affordance comes from the color token
|
|
// and the theme's :hover underline, keeping the card surface
|
|
// visually quiet.
|
|
$style = 'color:' . $palette['ink'] . ';text-decoration:none;';
|
|
|
|
return '<a href="' . e($url) . '" style="' . $style . '">' . e($name) . '</a>';
|
|
};
|
|
|
|
$record_label = static function (Fact $fact) use ($linked_name, $relationships, $palette): string {
|
|
$record = $fact->record();
|
|
|
|
$names = [];
|
|
$rels = [];
|
|
|
|
if ($record instanceof Individual) {
|
|
$names[] = $linked_name($record);
|
|
if (isset($relationships[$record->xref()])) {
|
|
$rels[] = strip_tags($relationships[$record->xref()]);
|
|
}
|
|
} elseif ($record instanceof Family) {
|
|
foreach ([$record->husband(), $record->wife()] as $spouse) {
|
|
if ($spouse instanceof Individual) {
|
|
$names[] = $linked_name($spouse);
|
|
if (isset($relationships[$spouse->xref()])) {
|
|
$rels[] = strip_tags($relationships[$spouse->xref()]);
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
return e($record->xref());
|
|
}
|
|
|
|
$html = implode(' & ', $names);
|
|
|
|
if ($rels !== []) {
|
|
$rels = array_values(array_unique(array_map(static fn ($r) => e($r), $rels)));
|
|
$html .= '<div style="margin-top:2px;color:' . $palette['ink3']
|
|
. ';font-style:italic;font-weight:400;font-size:13px;">'
|
|
. implode(' & ', $rels)
|
|
. '</div>';
|
|
}
|
|
|
|
return $html;
|
|
};
|
|
|
|
$event_date_display = static function (Fact $fact): string {
|
|
$date = $fact->date();
|
|
|
|
if (!$date instanceof Date || !$date->isOK()) {
|
|
return '';
|
|
}
|
|
|
|
return strip_tags($date->display());
|
|
};
|
|
|
|
$date_parts = static function (Fact $fact): array {
|
|
static $gregorian = null;
|
|
static $months_de = [
|
|
1 => 'Januar', 'Februar', 'März', 'April', 'Mai', 'Juni',
|
|
'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember',
|
|
];
|
|
static $months_en = [
|
|
1 => 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
|
|
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec',
|
|
];
|
|
|
|
$gregorian ??= new \Fisharebest\ExtCalendar\GregorianCalendar();
|
|
|
|
$date = $fact->date();
|
|
|
|
if (!$date->isOK()) {
|
|
return ['day_month' => '', 'year' => ''];
|
|
}
|
|
|
|
[$year, $month, $day] = $gregorian->jdToYmd($date->minimumJulianDay());
|
|
|
|
if (str_starts_with(I18N::languageTag(), 'de')) {
|
|
$day_month = $day . '. ' . mb_strtoupper($months_de[$month] ?? '');
|
|
} else {
|
|
$day_month = mb_strtoupper(($months_en[$month] ?? '') . ' ' . $day);
|
|
}
|
|
|
|
return ['day_month' => $day_month, 'year' => (string) $year];
|
|
};
|
|
|
|
$event_kind = static function (Fact $fact): string {
|
|
$parts = explode(':', $fact->tag());
|
|
|
|
return end($parts);
|
|
};
|
|
|
|
// SVG attribute values can't reference CSS custom properties directly
|
|
// — only the `style` attribute does — so each glyph carries its
|
|
// fill/stroke as inline style and inherits the right Nord shade from
|
|
// BockenTheme automatically.
|
|
$event_icon = static function (string $kind) use ($palette): string {
|
|
$svg_open = '<svg xmlns="http://www.w3.org/2000/svg" style="vertical-align:-3px;flex:none;" ';
|
|
|
|
return match ($kind) {
|
|
'BIRT' => $svg_open . 'viewBox="0 0 24 24" width="18" height="18">'
|
|
. '<path style="fill:' . $palette['birth'] . ';" 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>',
|
|
|
|
'DEAT' => $svg_open . 'viewBox="0 0 24 24" width="14" height="18">'
|
|
. '<rect x="10.5" y="2" width="3" height="20" style="fill:' . $palette['death'] . ';"/>'
|
|
. '<rect x="5.5" y="7" width="13" height="3" style="fill:' . $palette['death'] . ';"/>'
|
|
. '</svg>',
|
|
|
|
'MARR' => $svg_open . 'viewBox="0 0 36 22" width="28" height="18">'
|
|
. '<circle cx="13" cy="11" r="8" fill="none" style="stroke:' . $palette['marr'] . ';" stroke-width="1.8"/>'
|
|
. '<circle cx="23" cy="11" r="8" fill="none" style="stroke:' . $palette['marr'] . ';" stroke-width="1.8"/>'
|
|
. '</svg>',
|
|
|
|
default => '',
|
|
};
|
|
};
|
|
|
|
$avatar = static function (Individual|null $individual) use ($avatar_srcs, $avatar_size, $palette, $font_stack): string {
|
|
if (!$individual instanceof Individual) {
|
|
return '';
|
|
}
|
|
|
|
$alt = e(strip_tags($individual->fullName()));
|
|
|
|
if (isset($avatar_srcs[$individual->xref()])) {
|
|
$src = $avatar_srcs[$individual->xref()];
|
|
$inner = '<img src="' . e($src) . '" alt="' . $alt . '"'
|
|
. ' width="' . $avatar_size . '" height="' . $avatar_size . '"'
|
|
. ' style="border-radius:50%;object-fit:cover;display:block;">';
|
|
} else {
|
|
// Gendered silhouette placeholder — same artwork as the
|
|
// full-diagram plugin's `.photo-placeholder` + `.silhouette`
|
|
// shapes (see resources/css/full-diagram.css). Sex token
|
|
// gives the bg pastel; the silhouette stays the same shape
|
|
// regardless.
|
|
// Reuse the BockenTheme `.person-card .photo-placeholder` +
|
|
// `.silhouette` rules verbatim so the avatar shading matches
|
|
// the full-diagram plugin exactly (including dark mode).
|
|
$sex = strtolower($individual->sex());
|
|
$wrap_cls = 'person-card' . ($sex === 'm' ? ' sex-m' : ($sex === 'f' ? ' sex-f' : ''));
|
|
$inner = '<svg class="' . $wrap_cls . '" xmlns="http://www.w3.org/2000/svg"'
|
|
. ' viewBox="0 0 56 56"'
|
|
. ' width="' . $avatar_size . '" height="' . $avatar_size . '"'
|
|
. ' aria-label="' . $alt . '"'
|
|
. ' style="display:block;border-radius:50%;">'
|
|
. '<circle class="photo-placeholder" cx="28" cy="28" r="28"/>'
|
|
. '<circle class="silhouette" cx="28" cy="22" r="10"/>'
|
|
. '<ellipse class="silhouette" cx="28" cy="48" rx="16" ry="12"/>'
|
|
. '</svg>';
|
|
}
|
|
|
|
return '<a href="' . e($individual->url()) . '" style="text-decoration:none;">' . $inner . '</a>';
|
|
};
|
|
|
|
$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);
|
|
}
|
|
}
|
|
|
|
return '<table cellpadding="0" cellspacing="0" border="0"><tr>'
|
|
. '<td>' . ($parts[0] ?? '') . '</td>'
|
|
. (isset($parts[1]) ? '<td style="padding-left:8px;">' . $parts[1] . '</td>' : '')
|
|
. '</tr></table>';
|
|
}
|
|
|
|
return '';
|
|
};
|
|
|
|
$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',
|
|
};
|
|
};
|
|
|
|
$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);
|
|
};
|
|
|
|
$birthday_label = static function (int $age) use ($ordinal): string {
|
|
return $age > 0
|
|
? I18N::translate('%s birthday', $ordinal($age))
|
|
: I18N::translate('Birthday');
|
|
};
|
|
|
|
$anniversary_label = static function (int $age) use ($ordinal): string {
|
|
return $age > 0
|
|
? I18N::translate('%s wedding anniversary', $ordinal($age))
|
|
: I18N::translate('Wedding anniversary');
|
|
};
|
|
|
|
// ─── Row styles ─────────────────────────────────────────────────────────
|
|
|
|
$section_title_style = 'margin:0 0 4px;'
|
|
. 'font-family:' . $font_stack . ';'
|
|
. 'font-weight:400;font-size:22px;line-height:1.2;'
|
|
. 'color:' . $palette['ink'] . ';'
|
|
. 'letter-spacing:-0.005em;';
|
|
|
|
$section_kicker_style = 'margin:0 0 18px;'
|
|
. 'font-family:' . $font_stack . ';'
|
|
. 'font-style:italic;font-weight:300;font-size:14px;'
|
|
. 'color:' . $palette['ink3'] . ';';
|
|
|
|
$card_padding_y = 14;
|
|
$row_gap = 6;
|
|
|
|
// Card surface follows the IndividualPage facts-table treatment in
|
|
// BockenTheme: surface tone + 8 px radius + a soft drop shadow for
|
|
// the visual lift. The class hook `nl-card` carries everything so
|
|
// it survives Bootstrap's `.table-bordered` overrides — see the
|
|
// <style> block at the top of this view.
|
|
$card_outer_style = 'width:100%;border-radius:0.5rem;overflow:hidden;';
|
|
|
|
$avatar_inner_td = 'width:72px;vertical-align:middle;'
|
|
. 'padding:' . $card_padding_y . 'px 0 ' . $card_padding_y . 'px 18px;';
|
|
|
|
$content_inner_td = 'vertical-align:middle;'
|
|
. 'padding:' . $card_padding_y . 'px 18px;'
|
|
. 'font-family:' . $font_stack . ';font-size:15px;line-height:1.4;'
|
|
. 'font-weight:300;color:' . $palette['ink'] . ';';
|
|
|
|
$outer_card_td = 'vertical-align:middle;padding-bottom:' . $row_gap . 'px;';
|
|
$outer_gutter_td = 'width:16px;padding-bottom:' . $row_gap . 'px;';
|
|
// border-left lives on the `.nl-rail` class (see <style> block at the
|
|
// top of this view) so it survives the global Bootstrap-borders reset.
|
|
$outer_rail_td = 'width:1%;vertical-align:middle;'
|
|
. 'padding:0 4px ' . $row_gap . 'px 24px;'
|
|
. 'font-family:' . $font_stack . ';'
|
|
. 'color:' . $palette['ink3'] . ';white-space:nowrap;';
|
|
|
|
$summary_kicker_style = 'margin:24px 0 8px;'
|
|
. 'font-family:' . $font_stack . ';'
|
|
. 'font-weight:500;font-size:11px;letter-spacing:0.16em;text-transform:uppercase;'
|
|
. 'color:' . $palette['ink3'] . ';';
|
|
|
|
$summary_list_style = 'list-style:none;margin:0;padding:0;'
|
|
. 'font-family:' . $font_stack . ';font-size:13px;line-height:1.7;font-weight:300;'
|
|
. 'color:' . $palette['ink2'] . ';';
|
|
|
|
$summary_item_style = 'padding:3px 0;';
|
|
|
|
$event_row = static function (
|
|
Fact $fact,
|
|
string $body_html,
|
|
bool $show_dot,
|
|
)
|
|
use (
|
|
$record_avatars,
|
|
$date_parts,
|
|
$card_outer_style,
|
|
$avatar_inner_td,
|
|
$content_inner_td,
|
|
$outer_card_td,
|
|
$outer_gutter_td,
|
|
$outer_rail_td,
|
|
$palette,
|
|
): string {
|
|
$card_html =
|
|
'<table class="nl-card" role="presentation" cellpadding="0" cellspacing="0" border="0" '
|
|
. 'style="' . $card_outer_style . '"><tr>'
|
|
. '<td style="' . $avatar_inner_td . '">' . $record_avatars($fact) . '</td>'
|
|
. '<td style="' . $content_inner_td . '">' . $body_html . '</td>'
|
|
. '</tr></table>';
|
|
|
|
$parts = $date_parts($fact);
|
|
|
|
$dot_html = $show_dot
|
|
? '<span style="display:inline-block;width:14px;height:14px;background:'
|
|
. $palette['dot']
|
|
. ';border-radius:50%;margin-left:-33px;margin-right:16px;'
|
|
. 'vertical-align:middle;box-shadow:0 0 0 4px ' . $palette['bg'] . ';"></span>'
|
|
: '<span style="display:inline-block;width:14px;margin-left:-33px;'
|
|
. 'margin-right:16px;vertical-align:middle;"></span>';
|
|
|
|
if ($show_dot) {
|
|
$date_html =
|
|
'<span style="display:inline-block;vertical-align:middle;">'
|
|
. '<div style="font-weight:600;font-size:13px;letter-spacing:0.12em;color:'
|
|
. $palette['ink'] . ';">' . e($parts['day_month']) . '</div>'
|
|
. '<div style="font-weight:400;font-size:13px;color:'
|
|
. $palette['ink3'] . ';margin-top:2px;">' . e($parts['year']) . '</div>'
|
|
. '</span>';
|
|
} else {
|
|
$date_html =
|
|
'<span style="display:inline-block;vertical-align:middle;'
|
|
. 'font-weight:400;font-size:14px;color:' . $palette['ink2'] . ';">'
|
|
. e($parts['year'])
|
|
. '</span>';
|
|
}
|
|
|
|
return '<tr>'
|
|
. '<td style="' . $outer_card_td . '">' . $card_html . '</td>'
|
|
. '<td style="' . $outer_gutter_td . '"></td>'
|
|
. '<td class="nl-rail" style="' . $outer_rail_td . '">' . $dot_html . $date_html . '</td>'
|
|
. '</tr>';
|
|
};
|
|
|
|
$card_open = '<table role="presentation" cellpadding="0" cellspacing="0" border="0" '
|
|
. 'style="width:100%;border-collapse:collapse;">';
|
|
$card_close = '</table>';
|
|
|
|
$timeline_top_cap = '<tr>'
|
|
. '<td></td><td></td>'
|
|
. '<td class="nl-rail" style="height:4px;padding:0;'
|
|
. 'border-top-left-radius:4px;border-top-right-radius:4px;'
|
|
. 'font-size:0;line-height:0;"></td>'
|
|
. '</tr>';
|
|
|
|
$timeline_bottom_cap = '<tr>'
|
|
. '<td></td><td></td>'
|
|
. '<td class="nl-rail" style="height:18px;padding:0;'
|
|
. 'border-bottom-left-radius:4px;border-bottom-right-radius:4px;'
|
|
. 'font-size:0;line-height:0;"></td>'
|
|
. '</tr>';
|
|
|
|
// Stroke as inline style so the CSS variable resolves; the attribute
|
|
// form can only take a literal colour.
|
|
$arrow_svg = '<svg xmlns="http://www.w3.org/2000/svg" width="26" height="22"'
|
|
. ' viewBox="0 0 26 22" fill="none" style="stroke:' . $palette['rail'] . ';"'
|
|
. ' stroke-width="3" stroke-linecap="round" stroke-linejoin="round">'
|
|
. '<path d="M5 6 L13 16 L21 6"/>'
|
|
. '</svg>';
|
|
|
|
$timeline_arrow_row = '<tr>'
|
|
. '<td></td><td></td>'
|
|
. '<td style="height:32px;padding:0 0 0 24px;'
|
|
. 'font-size:0;line-height:0;vertical-align:top;text-align:left;">'
|
|
. '<span style="display:inline-block;margin-left:-37px;vertical-align:top;margin-top:-14px;">'
|
|
. $arrow_svg
|
|
. '</span>'
|
|
. '</td></tr>';
|
|
|
|
$nothing_to_show = $birthdays->isEmpty()
|
|
&& ($anniversaries === null || $anniversaries->isEmpty())
|
|
&& ($historical === null || $historical->isEmpty());
|
|
?>
|
|
|
|
<style>
|
|
/*
|
|
* BockenTheme applies `@extend .table; @extend .table-bordered;`
|
|
* to every <table> on the page, which (a) paints 1px hairline
|
|
* borders on every cell and (b) sets `--bs-table-bg` to the body
|
|
* bg so cells get repainted with the page colour — wiping out
|
|
* any inline card background. Reset both, scoped to this block.
|
|
* The timeline rail is then re-added via the `.nl-rail` class on
|
|
* the relevant TDs.
|
|
*/
|
|
.email-newsletter-block table {
|
|
--bs-table-bg: transparent;
|
|
margin: 0;
|
|
}
|
|
.email-newsletter-block table,
|
|
.email-newsletter-block tr,
|
|
.email-newsletter-block td,
|
|
.email-newsletter-block th {
|
|
border: 0 !important;
|
|
background-color: transparent;
|
|
color: inherit;
|
|
font-size: inherit;
|
|
}
|
|
/* Card surface — matches the IndividualPage `.wt-facts-table > tr`
|
|
treatment in BockenTheme: a quiet surface tone lifted by a soft
|
|
drop shadow. Cells inside are transparent so the table-level
|
|
bg shows through (and so Bootstrap's `--bs-table-bg` can't
|
|
repaint them with the page color). */
|
|
.email-newsletter-block .nl-card {
|
|
background: var(--color-surface, #efecea);
|
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
|
border-radius: 0.5rem;
|
|
}
|
|
.email-newsletter-block .nl-card > tbody > tr > td {
|
|
background-color: transparent;
|
|
}
|
|
/* Timeline rail — re-add the 4 px left border the global reset
|
|
killed. !important is needed to beat the reset above. */
|
|
.email-newsletter-block .nl-rail {
|
|
border-left: 4px solid var(--color-border) !important;
|
|
}
|
|
/* Avatar fallback piggybacks on BockenTheme's `.person-card`
|
|
placeholder rules (see theme.scss "FULL DIAGRAM PLUGIN"
|
|
section), so light/dark shading stays in sync with the
|
|
full-diagram plugin automatically — no styles needed here. */
|
|
</style>
|
|
|
|
<div class="email-newsletter-block"
|
|
style="max-width:760px;">
|
|
<?php if ($nothing_to_show) : ?>
|
|
<p style="margin:8px 0;color:<?= $palette['ink3'] ?>;font-style:italic;">
|
|
<?= e(I18N::translate('No upcoming family events in the next %d days.', $window_days)) ?>
|
|
</p>
|
|
<?php else : ?>
|
|
<table role="presentation" cellpadding="0" cellspacing="0" border="0"
|
|
style="width:100%;font-family:<?= $font_stack ?>;color:<?= $palette['ink'] ?>;">
|
|
|
|
<?php
|
|
// Merge living-kin events (birthdays + intact-couple
|
|
// anniversaries) into one date-sorted timeline. Icon
|
|
// and label key off the fact's tag so a mixed row of
|
|
// BIRT and MARR shares a single rail.
|
|
$living = collect($birthdays);
|
|
if ($include_anniversaries && $anniversaries !== null) {
|
|
$living = $living->merge($anniversaries);
|
|
}
|
|
$living = $living->sortBy(static fn (Fact $f): int => $f->jd ?? 0)->values();
|
|
?>
|
|
<?php if (!$living->isEmpty()) : ?>
|
|
<tr><td style="padding:8px 0 0;">
|
|
<h3 style="<?= $section_title_style ?>"><?= e(I18N::translate('Upcoming events')) ?></h3>
|
|
<p style="<?= $section_kicker_style ?>">
|
|
<?= e(I18N::translate('Birthdays of living kin and anniversaries of intact couples in the next %d days.', $window_days)) ?>
|
|
</p>
|
|
<?= $card_open ?>
|
|
<?= $timeline_top_cap ?>
|
|
<?php $prev_jd = null; ?>
|
|
<?php foreach ($living as $fact) : ?>
|
|
<?php
|
|
$kind = $event_kind($fact);
|
|
$age = $upcoming_age($fact);
|
|
$label = $kind === 'MARR' ? $anniversary_label($age) : $birthday_label($age);
|
|
$body = '<span style="display:inline-block;vertical-align:middle;margin-right:12px;">' . $event_icon($kind) . '</span>'
|
|
. '<span style="vertical-align:middle;">'
|
|
. '<span style="font-weight:600;color:' . $palette['ink'] . ';">' . $record_label($fact) . '</span>'
|
|
. '<div style="margin-top:2px;color:' . $palette['ink2'] . ';font-weight:300;">' . e($label) . '</div>'
|
|
. '</span>';
|
|
$show_dot = ($fact->jd ?? 0) !== $prev_jd;
|
|
$prev_jd = $fact->jd ?? 0;
|
|
echo $event_row($fact, $body, $show_dot);
|
|
?>
|
|
<?php endforeach ?>
|
|
<?= $timeline_bottom_cap ?>
|
|
<?= $timeline_arrow_row ?>
|
|
<?= $card_close ?>
|
|
</td></tr>
|
|
<?php endif ?>
|
|
|
|
<?php if ($historical !== null && !$historical->isEmpty()) : ?>
|
|
<tr><td style="padding:32px 0 0;">
|
|
<h3 style="<?= $section_title_style ?>"><?= e(I18N::translate('On this month in history')) ?></h3>
|
|
<p style="<?= $section_kicker_style ?>">
|
|
<?= e(I18N::translate('Events in the next %d days for people who have passed away.', $window_days)) ?>
|
|
</p>
|
|
<?= $card_open ?>
|
|
<?= $timeline_top_cap ?>
|
|
<?php $prev_jd = null; ?>
|
|
<?php foreach ($historical as $fact) : ?>
|
|
<?php
|
|
$kind = $event_kind($fact);
|
|
$body = '<span style="display:inline-block;vertical-align:middle;margin-right:12px;">' . $event_icon($kind) . '</span>'
|
|
. '<span style="vertical-align:middle;">'
|
|
. '<span style="font-weight:600;color:' . $palette['ink'] . ';">' . $record_label($fact) . '</span>'
|
|
. '<div style="margin-top:2px;color:' . $palette['ink2'] . ';font-weight:300;">' . e($fact->label()) . '</div>'
|
|
. '</span>';
|
|
$show_dot = ($fact->jd ?? 0) !== $prev_jd;
|
|
$prev_jd = $fact->jd ?? 0;
|
|
echo $event_row($fact, $body, $show_dot);
|
|
?>
|
|
<?php endforeach ?>
|
|
<?= $timeline_bottom_cap ?>
|
|
<?= $timeline_arrow_row ?>
|
|
<?= $card_close ?>
|
|
</td></tr>
|
|
<?php endif ?>
|
|
|
|
</table>
|
|
<?php endif ?>
|
|
</div>
|