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

317 lines
12 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?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;
}
}