317 lines
12 KiB
PHP
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('@', '@', $out);
|
||
$out = str_replace('.', '.', $out);
|
||
return $out;
|
||
}
|
||
|
||
}
|