Files
f7cloud_client/themes/forbion/dav_mail/InviteRenderer.php
T
root 8b6a0139db f7cloud_client
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-17 22:59:26 +00:00

317 lines
12 KiB
PHP

<?php
declare(strict_types=1);
namespace Forbion\Dav\Mail;
use Sabre\VObject\Component\VEvent;
final class InviteRenderer {
/**
* @param array{
* method: string,
* subject: string,
* data: array<string,mixed>,
* vevent: VEvent
* } $ctx
* @return array{subject?:string, html:string, text:string}
*/
public static function render(array $ctx): array {
$method = (string)($ctx['method'] ?? 'request');
$subject = (string)($ctx['subject'] ?? '');
$data = (array)($ctx['data'] ?? []);
/** @var VEvent $vevent */
$vevent = $ctx['vevent'];
// --- tokens (Figma) ---
$W = 564;
$border = '#D7D7D7';
$textMain = '#151515';
$textMuted = '#8A8A8A';
// --- brand/logo/icons ---
$logoUrl = (string)($data['logo_url'] ?? 'https://forbion.f7cloud.ru/themes/forbion/images/logo_gorizontal.png');
$brand = (string)($data['brand_name'] ?? 'Forbion F7');
$baseIcons = (string)($data['icons_base'] ?? 'https://forbion.f7cloud.ru/themes/forbion/images/');
$icons = [
'title' => (string)($data['icon_title'] ?? ($baseIcons . 'text.png')),
'when' => (string)($data['icon_when'] ?? ($baseIcons . 'date.png')),
'location' => (string)($data['icon_location'] ?? ($baseIcons . 'location.png')),
'description' => (string)($data['icon_description'] ?? ($baseIcons . 'notes.png')),
'link' => (string)($data['icon_link'] ?? ($baseIcons . 'text.png')),
'attendees' => (string)($data['icon_attendees'] ?? ($baseIcons . 'user.png')),
];
// --- main strings ---
$invitee = (string)($data['invitee_name'] ?? '');
$title = (string)($data['meeting_title'] ?? '');
$topLine = match (strtolower($method)) {
'cancel' => "{$invitee} отменил(а) событие «{$title}»",
'reply' => "{$invitee} ответил(а) на приглашение «{$title}»",
default => "{$invitee} приглашает вас принять участие в событии «{$title}»",
};
$heading = match (strtolower($method)) {
'cancel' => 'Отмена встречи',
'reply' => 'Ответ на приглашение',
default => 'Приглашение на встречу',
};
// fields
$when = (string)($data['meeting_when'] ?? '');
$locationText = (string)($data['meeting_location'] ?? '');
$locationHtml = (string)($data['meeting_location_html'] ?? htmlspecialchars($locationText, ENT_QUOTES));
$desc = (string)($data['meeting_description'] ?? '');
$linkText = (string)($data['meeting_url'] ?? '');
$linkHtml = (string)($data['meeting_url_html'] ?? htmlspecialchars($linkText, ENT_QUOTES));
// buttons
$acceptUrl = (string)($data['accept_url'] ?? '');
$declineUrl = (string)($data['decline_url'] ?? '');
$moreUrl = (string)($data['more_url'] ?? '');
$people = self::peopleLines($vevent);
$esc = static fn(string $s): string => htmlspecialchars($s, ENT_QUOTES);
// --- fonts: exactly like your site (TTF) ---
$fontCss = '
<style type="text/css">
@font-face {
font-family: "Raleway";
src: url("https://forbion.f7cloud.ru/themes/forbion/fonts/raleway-medium.ttf") format("truetype");
font-weight: 500;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "Raleway";
src: url("https://forbion.f7cloud.ru/themes/forbion/fonts/ralewayt.ttf") format("truetype");
font-weight: 600;
font-style: normal;
font-display: swap;
}
/* Global inside card */
.forbion-mail,
.forbion-mail * {
font-family: "Raleway", Arial, sans-serif !important;
font-variant-numeric: lining-nums;
}
/* Force links to look like text (esp. auto-linked emails) */
.forbion-mail a,
.forbion-mail a:visited,
.forbion-mail a:hover,
.forbion-mail a:active {
color: '.$textMain.' !important;
text-decoration: none !important;
}
/* if some client still converts to links */
.forbion-mail a[href^="mailto:"],
.forbion-mail a[x-apple-data-detectors],
.forbion-mail a[href^="tel:"],
.forbion-mail a[href^="sms:"] {
color: #151515 !important;
text-decoration: none !important;
border-bottom: 0 !important;
}
</style>';
$logoHtml = $logoUrl
? '<img src="'.$esc($logoUrl).'" alt="'.$esc($brand).'" style="display:block;margin:0 auto;border:0;outline:none;text-decoration:none;max-width:305px;height:auto;width:180px;">'
: '<div style="font-family:Raleway,Arial,sans-serif;font-weight:500;font-size:16px;line-height:20px;color:'.$textMain.';text-align:center;">'.$esc($brand).'</div>';
// Row builder: 12px between items, 4px between label/value
$row = function (string $iconUrl, string $label, string $valueHtml) use ($esc, $textMuted, $textMain): string {
if (trim(strip_tags($valueHtml)) === '') {
return '';
}
$icon = $iconUrl
? '<img src="'.$esc($iconUrl).'" width="20" height="20" alt="" style="display:block;width:20px;height:20px;border:0;outline:none;text-decoration:none;">'
: '';
return '
<tr>
<td style="padding:0 0 12px 0;">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0">
<tr>
<td width="20" valign="top" style="padding:0 12px 0 0;">'.$icon.'</td>
<td valign="top" style="padding:0;margin:0;">
<div style="font-family:Raleway,Arial,sans-serif;font-weight:500;font-size:16px;line-height:20px;color:'.$textMuted.';margin:0 0 4px 0;padding:0;">'.$esc($label).'</div>
<div style="font-family:Raleway,Arial,sans-serif;font-weight:500;font-size:14px;line-height:20px;color:'.$textMain.';margin:0;padding:0;text-decoration:none;">
<span style="color:'.$textMain.';text-decoration:none;">'.$valueHtml.'</span>
</div>
</td>
</tr>
</table>
</td>
</tr>';
};
$peopleHtml = $people ? implode('<br>', array_map([self::class, 'escNoLinkify'], $people)) : '';
// Buttons: height 36, radius 8, padding 8/12, min-width 107
$buttonsHtml = '';
if ($acceptUrl && $declineUrl) {
$buttonsHtml = '
<tr>
<td style="padding:20px 16px 0 16px;text-align:center;">
<a href="'.$esc($acceptUrl).'"
style="display:inline-block;min-width:107px;height:36px;padding:8px 12px;
box-sizing:border-box;border-radius:8px;
background:#76B82A;color:#ffffff;text-decoration:none;
font-family:Raleway,Arial,sans-serif;font-weight:500;font-size:14px;line-height:20px;
text-align:center;vertical-align:middle;margin-right:12px;">
Принять
</a>
<a href="'.$esc($declineUrl).'"
style="display:inline-block;min-width:107px;height:36px;padding:8px 12px;
box-sizing:border-box;border-radius:8px;
background:#EFEFEF;color:'.$textMain.';text-decoration:none;
font-family:Raleway,Arial,sans-serif;font-weight:500;font-size:14px;line-height:20px;
text-align:center;vertical-align:middle;">
Отклонить
</a>
</td>
</tr>';
if ($moreUrl) {
$buttonsHtml .= '
<tr>
<td style="padding:12px 16px 0 16px;text-align:center;">
<a href="'.$esc($moreUrl).'"
style="font-family:Raleway,Arial,sans-serif;font-weight:500;font-size:14px;line-height:20px;
color:'.$textMain.';text-decoration:underline;">
Ещё варианты
</a>
</td>
</tr>';
}
}
// --- HTML ---
// Required paddings:
// inside card: top/bottom 32, sides 16
// logo -> topLine: 20
// topLine -> heading: 36
// heading -> list: 20
$html = '<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">'.$fontCss.'</head>
<body style="margin:0;padding:0;background:#ffffff;">'.$fontCss.'
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background:#ffffff;padding:0;margin:0;">
<tr>
<td align="center" style="padding:16px 0;">
<table role="presentation" class="forbion-mail" width="'.$W.'" cellpadding="0" cellspacing="0"
style="width:'.$W.'px;max-width:'.$W.'px;background:#ffffff;border:1px solid '.$border.';border-radius:8px;overflow:hidden;">
<tr>
<td style="padding:32px 16px 0 16px;">
'.$logoHtml.'
</td>
</tr>
<tr>
<td style="padding:20px 16px 0 16px;text-align:center;
font-family:Raleway,Arial,sans-serif;font-weight:600;font-size:24px;line-height:26px;letter-spacing:0;color:'.$textMain.';">
'.$esc($topLine).'
</td>
</tr>
<tr>
<td style="padding:36px 16px 0 16px;text-align:left;
font-family:Raleway,Arial,sans-serif;font-weight:500;font-size:24px;line-height:20px;letter-spacing:0;color:'.$textMain.';">
'.$esc($heading).'
</td>
</tr>
<tr>
<td style="padding:20px 16px 0 16px;">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0">'.
$row($icons['title'], 'Название', $esc($title)).
$row($icons['when'], 'Когда', $esc($when)).
$row($icons['location'], 'Местонахождение', $locationText ? $locationHtml : '').
$row($icons['description'], 'Описание', $esc($desc)).
$row($icons['link'], 'Ссылка', $linkText ? $linkHtml : '').
$row($icons['attendees'], 'Кто', $peopleHtml).
'</table>
</td>
</tr>
'.$buttonsHtml.'
<tr>
<td style="padding:20px 16px 32px 16px;text-align:center;
font-family:Raleway,Arial,sans-serif;font-weight:500;font-size:12px;line-height:18px;color:'.$textMuted.';">
Это автоматическое письмо календаря '.$esc($brand).'.
</td>
</tr>
</table>
</td>
</tr>
</table>
</body></html>';
// --- TEXT ---
$text = $topLine . "\n\n" .
$heading . "\n" .
"Название: {$title}\n" .
($when ? "Когда: {$when}\n" : '') .
($locationText ? "Местонахождение: {$locationText}\n" : '') .
($desc ? "Описание: {$desc}\n" : '') .
($linkText ? "Ссылка: {$linkText}\n" : '');
if ($people) {
$text .= "\nКто:\n - " . implode("\n - ", $people) . "\n";
}
if ($acceptUrl && $declineUrl) {
$text .= "\nПринять: {$acceptUrl}\nОтклонить: {$declineUrl}\n";
if ($moreUrl) {
$text .= "Ещё варианты: {$moreUrl}\n";
}
}
return ['subject' => $subject, 'html' => $html, 'text' => $text];
}
/** @return string[] */
private static function peopleLines(VEvent $vevent): array {
$out = [];
if (isset($vevent->ORGANIZER)) {
$org = $vevent->ORGANIZER;
$orgEmail = self::mailtoToEmail((string)$org->getNormalizedValue());
$orgName = isset($org->CN) ? (string)$org->CN->getValue() : '';
$out[] = trim(($orgName ? $orgName.' ' : '').'<' . $orgEmail . '>') . ' (организатор)';
}
foreach ($vevent->select('ATTENDEE') as $att) {
$email = self::mailtoToEmail((string)$att->getNormalizedValue());
$name = isset($att['CN']) ? (string)$att['CN']->getValue() : '';
$line = trim(($name ? $name.' ' : '').'<' . $email . '>');
if ($line) {
$out[] = $line;
}
}
return array_values(array_unique($out));
}
private static function mailtoToEmail(string $mailto): string {
return str_starts_with($mailto, 'mailto:') ? substr($mailto, 7) : $mailto;
}
private static function escNoLinkify(string $s): string {
$out = htmlspecialchars($s, ENT_QUOTES);
$out = str_replace('@', '&#64;', $out);
$out = str_replace('.', '&#46;', $out);
return $out;
}
}