docs: render email previews from templates
This commit is contained in:
@@ -0,0 +1,406 @@
|
|||||||
|
import json
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
from types import SimpleNamespace
|
||||||
|
|
||||||
|
REPO_ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
BACKEND_ROOT = REPO_ROOT / "backend"
|
||||||
|
sys.path.insert(0, str(BACKEND_ROOT))
|
||||||
|
if hasattr(sys.stdout, "reconfigure"):
|
||||||
|
sys.stdout.reconfigure(encoding="utf-8")
|
||||||
|
|
||||||
|
from bot.middlewares.i18n import JsonI18n # noqa: E402
|
||||||
|
from bot.services.email_templates import ( # noqa: E402
|
||||||
|
render_account_merged,
|
||||||
|
render_login_code,
|
||||||
|
render_payment_success,
|
||||||
|
render_subscription_expiring,
|
||||||
|
render_subscription_lifecycle_notification,
|
||||||
|
render_support_admin_reply_user,
|
||||||
|
render_support_new_ticket_admin,
|
||||||
|
render_support_ticket_closed_user,
|
||||||
|
render_support_user_reply_admin,
|
||||||
|
render_user_notification,
|
||||||
|
)
|
||||||
|
|
||||||
|
LANGUAGE = "ru"
|
||||||
|
|
||||||
|
|
||||||
|
def settings():
|
||||||
|
return SimpleNamespace(
|
||||||
|
DEFAULT_LANGUAGE=LANGUAGE,
|
||||||
|
EMAIL_CODE_TTL_SECONDS=600,
|
||||||
|
WEBAPP_LOGO_URL="",
|
||||||
|
WEBAPP_LOGO_USE_EMOJI=False,
|
||||||
|
WEBAPP_PRIMARY_COLOR="#00fe7a",
|
||||||
|
WEBAPP_TITLE="remnawave-minishop",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
I18N = JsonI18n(str(REPO_ROOT / "locales"), default=LANGUAGE)
|
||||||
|
SETTINGS = settings()
|
||||||
|
SAMPLE = {
|
||||||
|
"amount": 390,
|
||||||
|
"code": "483921",
|
||||||
|
"currency": "RUB",
|
||||||
|
"dashboard_url": "https://mini.example.com/app",
|
||||||
|
"end_date": "21.06.2026, 18:00",
|
||||||
|
"magic_url": "https://mini.example.com/app/auth/magic/preview",
|
||||||
|
"premium_traffic": 25,
|
||||||
|
"regular_traffic": 100,
|
||||||
|
"ticket_url": "https://mini.example.com/app/support/42",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def t(key: str, **kwargs) -> str:
|
||||||
|
return I18N.gettext(LANGUAGE, key, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def preview(item_id: str, category: str, title: str, content):
|
||||||
|
return {
|
||||||
|
"id": item_id,
|
||||||
|
"category": category,
|
||||||
|
"title": title,
|
||||||
|
"subject": content.subject,
|
||||||
|
"html": content.html,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def payment_preview(
|
||||||
|
item_id: str,
|
||||||
|
title: str,
|
||||||
|
sale_mode: str,
|
||||||
|
*,
|
||||||
|
months: int = 0,
|
||||||
|
traffic_gb: float | None = None,
|
||||||
|
):
|
||||||
|
return preview(
|
||||||
|
item_id,
|
||||||
|
"Платежи",
|
||||||
|
title,
|
||||||
|
render_payment_success(
|
||||||
|
SETTINGS,
|
||||||
|
language_code=LANGUAGE,
|
||||||
|
sale_mode=sale_mode,
|
||||||
|
months=months,
|
||||||
|
traffic_gb=traffic_gb,
|
||||||
|
amount=SAMPLE["amount"],
|
||||||
|
currency=SAMPLE["currency"],
|
||||||
|
end_date_text=SAMPLE["end_date"],
|
||||||
|
dashboard_url=SAMPLE["dashboard_url"],
|
||||||
|
provider_label="YooKassa",
|
||||||
|
i18n=I18N,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def user_notification_preview(
|
||||||
|
item_id: str,
|
||||||
|
title: str,
|
||||||
|
subject_key: str,
|
||||||
|
message_text: str,
|
||||||
|
*,
|
||||||
|
cta_label_key: str = "email_user_notification_cta",
|
||||||
|
):
|
||||||
|
subject = t(subject_key)
|
||||||
|
return preview(
|
||||||
|
item_id,
|
||||||
|
"Уведомления",
|
||||||
|
title,
|
||||||
|
render_user_notification(
|
||||||
|
SETTINGS,
|
||||||
|
language_code=LANGUAGE,
|
||||||
|
subject=subject,
|
||||||
|
heading=subject,
|
||||||
|
intro=t("email_user_notification_intro"),
|
||||||
|
message_text=message_text,
|
||||||
|
dashboard_url=SAMPLE["dashboard_url"],
|
||||||
|
cta_label=t(cta_label_key),
|
||||||
|
i18n=I18N,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def expiring_preview(item_id: str, title: str, days_left: int):
|
||||||
|
return preview(
|
||||||
|
item_id,
|
||||||
|
"Подписка",
|
||||||
|
title,
|
||||||
|
render_subscription_expiring(
|
||||||
|
SETTINGS,
|
||||||
|
language_code=LANGUAGE,
|
||||||
|
days_left=days_left,
|
||||||
|
end_date_text=SAMPLE["end_date"],
|
||||||
|
dashboard_url=SAMPLE["dashboard_url"],
|
||||||
|
i18n=I18N,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def lifecycle_preview(
|
||||||
|
item_id: str,
|
||||||
|
title: str,
|
||||||
|
notification_key: str,
|
||||||
|
message_text: str,
|
||||||
|
*,
|
||||||
|
mirrored_from_telegram: bool = False,
|
||||||
|
days_left: int | None = None,
|
||||||
|
hours_before: int | None = None,
|
||||||
|
):
|
||||||
|
return preview(
|
||||||
|
item_id,
|
||||||
|
"Подписка",
|
||||||
|
title,
|
||||||
|
render_subscription_lifecycle_notification(
|
||||||
|
SETTINGS,
|
||||||
|
language_code=LANGUAGE,
|
||||||
|
notification_key=notification_key,
|
||||||
|
message_text=message_text,
|
||||||
|
end_date_text=SAMPLE["end_date"],
|
||||||
|
dashboard_url=SAMPLE["dashboard_url"],
|
||||||
|
mirrored_from_telegram=mirrored_from_telegram,
|
||||||
|
days_left=days_left,
|
||||||
|
hours_before=hours_before,
|
||||||
|
i18n=I18N,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def support_snapshot_rows():
|
||||||
|
return [
|
||||||
|
("email_support_row_tariff", "Premium"),
|
||||||
|
("email_support_row_remaining", "3 д. 4 ч."),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
EMAIL_PREVIEWS = [
|
||||||
|
preview(
|
||||||
|
"login-code",
|
||||||
|
"Доступ",
|
||||||
|
"Код для входа",
|
||||||
|
render_login_code(
|
||||||
|
SETTINGS,
|
||||||
|
code=SAMPLE["code"],
|
||||||
|
language_code=LANGUAGE,
|
||||||
|
magic_link=SAMPLE["magic_url"],
|
||||||
|
purpose="login",
|
||||||
|
i18n=I18N,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
preview(
|
||||||
|
"set-password-code",
|
||||||
|
"Доступ",
|
||||||
|
"Код для создания пароля",
|
||||||
|
render_login_code(
|
||||||
|
SETTINGS,
|
||||||
|
code=SAMPLE["code"],
|
||||||
|
language_code=LANGUAGE,
|
||||||
|
purpose="set_password",
|
||||||
|
i18n=I18N,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
preview(
|
||||||
|
"account-merged",
|
||||||
|
"Аккаунт",
|
||||||
|
"Аккаунты объединены",
|
||||||
|
render_account_merged(
|
||||||
|
SETTINGS,
|
||||||
|
language_code=LANGUAGE,
|
||||||
|
primary_user_id=100200300,
|
||||||
|
removed_user_id=-42,
|
||||||
|
final_end_date_text=SAMPLE["end_date"],
|
||||||
|
i18n=I18N,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
payment_preview(
|
||||||
|
"payment-subscription",
|
||||||
|
"Оплата подписки",
|
||||||
|
"subscription",
|
||||||
|
months=1,
|
||||||
|
),
|
||||||
|
payment_preview(
|
||||||
|
"payment-traffic",
|
||||||
|
"Покупка трафика",
|
||||||
|
"traffic",
|
||||||
|
traffic_gb=SAMPLE["regular_traffic"],
|
||||||
|
),
|
||||||
|
payment_preview(
|
||||||
|
"payment-premium-traffic",
|
||||||
|
"Покупка premium-трафика",
|
||||||
|
"premium_topup",
|
||||||
|
traffic_gb=SAMPLE["premium_traffic"],
|
||||||
|
),
|
||||||
|
payment_preview("payment-hwid", "Покупка HWID-устройств", "hwid_device", months=2),
|
||||||
|
payment_preview("payment-tariff-upgrade", "Платное повышение тарифа", "tariff_upgrade"),
|
||||||
|
user_notification_preview(
|
||||||
|
"payment-failed",
|
||||||
|
"Неуспешная оплата",
|
||||||
|
"email_payment_failed_subject",
|
||||||
|
"Платеж не был завершен. Можно попробовать еще раз из личного кабинета.",
|
||||||
|
),
|
||||||
|
user_notification_preview(
|
||||||
|
"payment-method-bound",
|
||||||
|
"Способ оплаты привязан",
|
||||||
|
"email_payment_method_bound_subject",
|
||||||
|
"Автопродление подключено, следующий платеж пройдет автоматически.",
|
||||||
|
),
|
||||||
|
user_notification_preview(
|
||||||
|
"referral-bonus",
|
||||||
|
"Реферальный бонус",
|
||||||
|
"email_referral_bonus_subject",
|
||||||
|
"Друг активировал подписку, и бонусные дни уже добавлены к вашему аккаунту.",
|
||||||
|
),
|
||||||
|
user_notification_preview(
|
||||||
|
"trial-traffic-depleted",
|
||||||
|
"Трафик пробного периода закончился",
|
||||||
|
"email_trial_traffic_depleted_subject",
|
||||||
|
"Пробный трафик израсходован. Оформите подписку, чтобы продолжить пользоваться сервисом.",
|
||||||
|
),
|
||||||
|
user_notification_preview(
|
||||||
|
"regular-traffic-almost",
|
||||||
|
"Обычный трафик почти закончился",
|
||||||
|
"email_traffic_warning_regular_almost_subject",
|
||||||
|
"Использовано больше 85% трафика тарифа. Можно докупить пакет заранее.",
|
||||||
|
cta_label_key="email_traffic_warning_regular_cta",
|
||||||
|
),
|
||||||
|
user_notification_preview(
|
||||||
|
"regular-traffic-depleted",
|
||||||
|
"Обычный трафик закончился",
|
||||||
|
"email_traffic_warning_regular_depleted_subject",
|
||||||
|
"Трафик тарифа израсходован. Докупите пакет, чтобы восстановить доступ.",
|
||||||
|
cta_label_key="email_traffic_warning_regular_cta",
|
||||||
|
),
|
||||||
|
user_notification_preview(
|
||||||
|
"premium-traffic-almost",
|
||||||
|
"Premium-трафик почти закончился",
|
||||||
|
"email_traffic_warning_premium_almost_subject",
|
||||||
|
"Premium-трафика осталось мало. Можно докупить пакет до полного расхода.",
|
||||||
|
cta_label_key="email_traffic_warning_premium_cta",
|
||||||
|
),
|
||||||
|
user_notification_preview(
|
||||||
|
"premium-traffic-depleted",
|
||||||
|
"Premium-трафик закончился",
|
||||||
|
"email_traffic_warning_premium_depleted_subject",
|
||||||
|
(
|
||||||
|
"Premium-трафик израсходован. Докупите пакет, "
|
||||||
|
"чтобы продолжить использовать premium-маршруты."
|
||||||
|
),
|
||||||
|
cta_label_key="email_traffic_warning_premium_cta",
|
||||||
|
),
|
||||||
|
expiring_preview(
|
||||||
|
"subscription-expiring-today",
|
||||||
|
"Подписка заканчивается сегодня",
|
||||||
|
0,
|
||||||
|
),
|
||||||
|
expiring_preview(
|
||||||
|
"subscription-expiring-tomorrow",
|
||||||
|
"Подписка заканчивается завтра",
|
||||||
|
1,
|
||||||
|
),
|
||||||
|
expiring_preview(
|
||||||
|
"subscription-expiring-days",
|
||||||
|
"Подписка скоро закончится",
|
||||||
|
3,
|
||||||
|
),
|
||||||
|
lifecycle_preview(
|
||||||
|
"lifecycle-before-days",
|
||||||
|
"Lifecycle: осталось несколько дней",
|
||||||
|
"before_days",
|
||||||
|
"Подписка скоро закончится. Продлите ее заранее, чтобы доступ не прерывался.",
|
||||||
|
days_left=3,
|
||||||
|
),
|
||||||
|
lifecycle_preview(
|
||||||
|
"lifecycle-before-hours",
|
||||||
|
"Lifecycle: осталось несколько часов",
|
||||||
|
"before_hours",
|
||||||
|
"До окончания подписки осталось несколько часов.",
|
||||||
|
hours_before=6,
|
||||||
|
),
|
||||||
|
lifecycle_preview(
|
||||||
|
"lifecycle-expired",
|
||||||
|
"Lifecycle: подписка закончилась",
|
||||||
|
"expired",
|
||||||
|
"Подписка закончилась. Продлите доступ в личном кабинете.",
|
||||||
|
),
|
||||||
|
lifecycle_preview(
|
||||||
|
"lifecycle-expired-after",
|
||||||
|
"Lifecycle: подписка закончилась вчера",
|
||||||
|
"expired_24h_after",
|
||||||
|
"Вчера подписка была отключена. Вы можете восстановить доступ продлением.",
|
||||||
|
),
|
||||||
|
lifecycle_preview(
|
||||||
|
"lifecycle-autorenew",
|
||||||
|
"Lifecycle: автопродление завтра",
|
||||||
|
"before_2d_autorenew",
|
||||||
|
"Завтра будет выполнено автопродление подписки.",
|
||||||
|
),
|
||||||
|
lifecycle_preview(
|
||||||
|
"lifecycle-mirrored",
|
||||||
|
"Lifecycle: копия Telegram-уведомления",
|
||||||
|
"before_days",
|
||||||
|
"Это письмо дублирует важное уведомление, отправленное в Telegram.",
|
||||||
|
mirrored_from_telegram=True,
|
||||||
|
days_left=2,
|
||||||
|
),
|
||||||
|
preview(
|
||||||
|
"support-new-ticket-admin",
|
||||||
|
"Поддержка",
|
||||||
|
"Новый тикет для администратора",
|
||||||
|
render_support_new_ticket_admin(
|
||||||
|
SETTINGS,
|
||||||
|
I18N,
|
||||||
|
LANGUAGE,
|
||||||
|
ticket_id=42,
|
||||||
|
user_display="alex@example.com",
|
||||||
|
subject="Не работает подключение",
|
||||||
|
body_preview="Пользователь не может подключиться после продления.",
|
||||||
|
snapshot_rows=support_snapshot_rows(),
|
||||||
|
ticket_url="https://mini.example.com/app/admin/support/42",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
preview(
|
||||||
|
"support-user-reply-admin",
|
||||||
|
"Поддержка",
|
||||||
|
"Ответ пользователя для администратора",
|
||||||
|
render_support_user_reply_admin(
|
||||||
|
SETTINGS,
|
||||||
|
I18N,
|
||||||
|
LANGUAGE,
|
||||||
|
ticket_id=42,
|
||||||
|
user_display="alex@example.com",
|
||||||
|
subject="Не работает подключение",
|
||||||
|
body_preview="Проблема повторилась на телефоне и ноутбуке.",
|
||||||
|
snapshot_rows=support_snapshot_rows(),
|
||||||
|
ticket_url="https://mini.example.com/app/admin/support/42",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
preview(
|
||||||
|
"support-admin-reply-user",
|
||||||
|
"Поддержка",
|
||||||
|
"Ответ поддержки пользователю",
|
||||||
|
render_support_admin_reply_user(
|
||||||
|
SETTINGS,
|
||||||
|
I18N,
|
||||||
|
LANGUAGE,
|
||||||
|
ticket_id=42,
|
||||||
|
subject="Не работает подключение",
|
||||||
|
body_preview="Мы обновили конфигурацию. Попробуйте подключиться еще раз.",
|
||||||
|
ticket_url=SAMPLE["ticket_url"],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
preview(
|
||||||
|
"support-ticket-closed-user",
|
||||||
|
"Поддержка",
|
||||||
|
"Тикет закрыт",
|
||||||
|
render_support_ticket_closed_user(
|
||||||
|
SETTINGS,
|
||||||
|
I18N,
|
||||||
|
LANGUAGE,
|
||||||
|
ticket_id=42,
|
||||||
|
subject="Не работает подключение",
|
||||||
|
ticket_url=SAMPLE["ticket_url"],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
print(json.dumps(EMAIL_PREVIEWS, ensure_ascii=False))
|
||||||
@@ -1,463 +1,55 @@
|
|||||||
import { existsSync, readFileSync } from "node:fs";
|
import { spawnSync } from "node:child_process";
|
||||||
|
import { existsSync } from "node:fs";
|
||||||
import { resolve } from "node:path";
|
import { resolve } from "node:path";
|
||||||
|
|
||||||
const localePathCandidates = [
|
const repoRootCandidates = [
|
||||||
resolve(process.cwd(), "..", "locales", "ru.json"),
|
resolve(process.cwd(), ".."),
|
||||||
resolve(process.cwd(), "locales", "ru.json"),
|
resolve(process.cwd()),
|
||||||
];
|
];
|
||||||
const localePath =
|
const repoRoot =
|
||||||
localePathCandidates.find((candidate) => existsSync(candidate)) ||
|
repoRootCandidates.find(
|
||||||
localePathCandidates[0];
|
(candidate) =>
|
||||||
const messages = JSON.parse(readFileSync(localePath, "utf8"));
|
existsSync(resolve(candidate, "backend")) &&
|
||||||
|
existsSync(resolve(candidate, "docs-site")),
|
||||||
const sample = {
|
) || repoRootCandidates[0];
|
||||||
amount: "390 RUB",
|
const generatorPath = resolve(
|
||||||
brand: "remnawave-minishop",
|
repoRoot,
|
||||||
code: "483921",
|
"docs-site",
|
||||||
dashboardUrl: "https://mini.example.com/app",
|
"scripts",
|
||||||
endDate: "21.06.2026, 18:00",
|
"generate-email-previews.py",
|
||||||
magicUrl: "https://mini.example.com/app/auth/magic/preview",
|
|
||||||
minutes: 10,
|
|
||||||
premiumTraffic: "25",
|
|
||||||
regularTraffic: "100",
|
|
||||||
ticketUrl: "https://mini.example.com/app/support/42",
|
|
||||||
};
|
|
||||||
|
|
||||||
const interpolate = (template, values = {}) =>
|
|
||||||
String(template || "").replace(/\{([a-zA-Z0-9_]+)\}/g, (match, name) =>
|
|
||||||
Object.prototype.hasOwnProperty.call(values, name) ? values[name] : match,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const t = (key, values = {}) => interpolate(messages[key] || key, values);
|
const pythonCommands = [
|
||||||
|
process.env.PYTHON,
|
||||||
|
process.platform === "win32" ? "python" : "python3",
|
||||||
|
"python",
|
||||||
|
].filter(Boolean);
|
||||||
|
|
||||||
const escapeHtml = (value) =>
|
let lastError = "";
|
||||||
String(value || "")
|
let generated = null;
|
||||||
.replace(/&/g, "&")
|
for (const command of pythonCommands) {
|
||||||
.replace(/</g, "<")
|
const result = spawnSync(command, [generatorPath], {
|
||||||
.replace(/>/g, ">")
|
cwd: repoRoot,
|
||||||
.replace(/"/g, """);
|
encoding: "utf8",
|
||||||
|
env: {
|
||||||
const textWithBreaks = (value) => escapeHtml(value).replace(/\n/g, "<br>");
|
...process.env,
|
||||||
|
PYTHONIOENCODING: "utf-8",
|
||||||
const stripTags = (value) =>
|
},
|
||||||
String(value || "")
|
});
|
||||||
.replace(/<br\s*\/?>/gi, "\n")
|
if (result.status === 0 && result.stdout) {
|
||||||
.replace(/<[^>]*>/g, "");
|
generated = result.stdout;
|
||||||
|
break;
|
||||||
const renderRows = (rows = []) => {
|
|
||||||
if (!rows.length) return "";
|
|
||||||
return `<table class="mail-card__rows" role="presentation" cellpadding="0" cellspacing="0">
|
|
||||||
${rows
|
|
||||||
.map(
|
|
||||||
([label, value]) => `<tr>
|
|
||||||
<td>${escapeHtml(label)}</td>
|
|
||||||
<td>${escapeHtml(value)}</td>
|
|
||||||
</tr>`,
|
|
||||||
)
|
|
||||||
.join("")}
|
|
||||||
</table>`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderEmailHtml = ({
|
|
||||||
subject,
|
|
||||||
heading = subject,
|
|
||||||
intro,
|
|
||||||
rows,
|
|
||||||
code,
|
|
||||||
message,
|
|
||||||
note,
|
|
||||||
ctaLabel,
|
|
||||||
ctaUrl,
|
|
||||||
}) => `<div class="mail-card">
|
|
||||||
<div class="mail-card__brand">${escapeHtml(sample.brand)}</div>
|
|
||||||
<div class="mail-card__panel">
|
|
||||||
<p class="mail-card__subject">${escapeHtml(subject)}</p>
|
|
||||||
<h2>${escapeHtml(heading)}</h2>
|
|
||||||
${intro ? `<p class="mail-card__intro">${textWithBreaks(intro)}</p>` : ""}
|
|
||||||
${
|
|
||||||
code
|
|
||||||
? `<div class="mail-card__code" aria-label="Email code">${escapeHtml(code)}</div>`
|
|
||||||
: ""
|
|
||||||
}
|
}
|
||||||
${renderRows(rows)}
|
lastError = [result.error?.message, result.stderr, result.stdout]
|
||||||
${
|
.filter(Boolean)
|
||||||
message
|
.join("\n")
|
||||||
? `<div class="mail-card__message">${textWithBreaks(message)}</div>`
|
.trim();
|
||||||
: ""
|
|
||||||
}
|
}
|
||||||
${
|
|
||||||
ctaLabel
|
if (!generated) {
|
||||||
? `<a class="mail-card__cta" href="${escapeHtml(ctaUrl || sample.dashboardUrl)}">${escapeHtml(
|
throw new Error(
|
||||||
ctaLabel,
|
`Failed to generate email previews from backend templates.\n${lastError}`,
|
||||||
)}</a>`
|
);
|
||||||
: ""
|
|
||||||
}
|
}
|
||||||
${note ? `<p class="mail-card__note">${textWithBreaks(note)}</p>` : ""}
|
|
||||||
</div>
|
|
||||||
<p class="mail-card__footer">${escapeHtml(t("email_footer_auto", { brand: sample.brand }))}</p>
|
|
||||||
</div>`;
|
|
||||||
|
|
||||||
const preview = (item) => ({
|
export const emailPreviews = JSON.parse(generated);
|
||||||
...item,
|
|
||||||
html: renderEmailHtml(item),
|
|
||||||
});
|
|
||||||
|
|
||||||
const paymentRows = (periodLabel, periodValue, provider = "YooKassa") => [
|
|
||||||
[periodLabel, periodValue],
|
|
||||||
[t("email_payment_success_row_amount"), sample.amount],
|
|
||||||
[t("email_payment_success_row_end_date"), sample.endDate],
|
|
||||||
[t("email_payment_success_row_method"), provider],
|
|
||||||
];
|
|
||||||
|
|
||||||
const paymentPreview = ({
|
|
||||||
id,
|
|
||||||
title,
|
|
||||||
introKey,
|
|
||||||
periodLabel,
|
|
||||||
periodValue,
|
|
||||||
textKey,
|
|
||||||
values = {},
|
|
||||||
}) =>
|
|
||||||
preview({
|
|
||||||
id,
|
|
||||||
category: "Платежи",
|
|
||||||
title,
|
|
||||||
subject: t("email_payment_success_subject"),
|
|
||||||
heading: t("email_payment_success_heading"),
|
|
||||||
intro: t(introKey, values),
|
|
||||||
rows: paymentRows(periodLabel, periodValue),
|
|
||||||
message: t(textKey, {
|
|
||||||
amount: sample.amount,
|
|
||||||
end_date: sample.endDate,
|
|
||||||
...values,
|
|
||||||
}),
|
|
||||||
note: t("email_payment_success_footer_note"),
|
|
||||||
ctaLabel: t("email_payment_success_cta"),
|
|
||||||
});
|
|
||||||
|
|
||||||
const notificationPreview = ({
|
|
||||||
id,
|
|
||||||
title,
|
|
||||||
subjectKey,
|
|
||||||
message,
|
|
||||||
ctaKey = "email_user_notification_cta",
|
|
||||||
}) =>
|
|
||||||
preview({
|
|
||||||
id,
|
|
||||||
category: "Уведомления",
|
|
||||||
title,
|
|
||||||
subject: t(subjectKey),
|
|
||||||
heading: t(subjectKey),
|
|
||||||
intro: t("email_user_notification_intro"),
|
|
||||||
message,
|
|
||||||
ctaLabel: t(ctaKey),
|
|
||||||
});
|
|
||||||
|
|
||||||
const expiringPreview = ({ id, title, suffix, days }) =>
|
|
||||||
preview({
|
|
||||||
id,
|
|
||||||
category: "Подписка",
|
|
||||||
title,
|
|
||||||
subject: t(`email_subscription_expiring_subject_${suffix}`, { days }),
|
|
||||||
heading: t(`email_subscription_expiring_heading_${suffix}`, { days }),
|
|
||||||
intro: t(`email_subscription_expiring_intro_${suffix}`, { days }),
|
|
||||||
rows: [
|
|
||||||
[t("email_subscription_expiring_row_days_left"), String(days)],
|
|
||||||
[t("email_subscription_expiring_row_end_date"), sample.endDate],
|
|
||||||
],
|
|
||||||
message: t("email_subscription_expiring_text", {
|
|
||||||
heading: t(`email_subscription_expiring_heading_${suffix}`, { days }),
|
|
||||||
end_date: sample.endDate,
|
|
||||||
}),
|
|
||||||
note: t("email_subscription_expiring_note"),
|
|
||||||
ctaLabel: t("email_subscription_expiring_cta"),
|
|
||||||
});
|
|
||||||
|
|
||||||
const lifecycleSubject = (key, values = {}) => t(key, values);
|
|
||||||
|
|
||||||
const lifecyclePreview = ({
|
|
||||||
id,
|
|
||||||
title,
|
|
||||||
subject,
|
|
||||||
introKey = "email_subscription_lifecycle_intro_direct",
|
|
||||||
message,
|
|
||||||
}) =>
|
|
||||||
preview({
|
|
||||||
id,
|
|
||||||
category: "Подписка",
|
|
||||||
title,
|
|
||||||
subject,
|
|
||||||
heading: subject,
|
|
||||||
intro: t(introKey),
|
|
||||||
rows: [[t("email_subscription_lifecycle_row_end_date"), sample.endDate]],
|
|
||||||
message,
|
|
||||||
ctaLabel: t("email_subscription_lifecycle_cta"),
|
|
||||||
});
|
|
||||||
|
|
||||||
const supportRows = (includeUser = false) => [
|
|
||||||
[t("email_support_row_ticket"), "#42"],
|
|
||||||
...(includeUser ? [[t("email_support_row_user"), "alex@example.com"]] : []),
|
|
||||||
[t("email_support_row_subject"), "Не работает подключение"],
|
|
||||||
[t("email_support_row_tariff"), "Premium"],
|
|
||||||
[t("email_support_row_remaining"), "3 д. 4 ч."],
|
|
||||||
];
|
|
||||||
|
|
||||||
export const emailPreviews = [
|
|
||||||
preview({
|
|
||||||
id: "login-code",
|
|
||||||
category: "Доступ",
|
|
||||||
title: "Код для входа",
|
|
||||||
subject: t("email_login_code_subject", { code: sample.code }),
|
|
||||||
heading: t("email_login_code_heading"),
|
|
||||||
intro: t("email_login_code_intro"),
|
|
||||||
code: sample.code,
|
|
||||||
message: [
|
|
||||||
stripTags(t("email_login_code_expiry_html", { minutes: sample.minutes })),
|
|
||||||
t("email_login_code_security"),
|
|
||||||
t("email_login_code_text_magic", { url: sample.magicUrl }),
|
|
||||||
].join("\n\n"),
|
|
||||||
ctaLabel: t("email_login_code_magic_cta"),
|
|
||||||
ctaUrl: sample.magicUrl,
|
|
||||||
}),
|
|
||||||
preview({
|
|
||||||
id: "set-password-code",
|
|
||||||
category: "Доступ",
|
|
||||||
title: "Код для создания пароля",
|
|
||||||
subject: t("email_set_password_code_subject", { code: sample.code }),
|
|
||||||
heading: t("email_set_password_code_heading"),
|
|
||||||
intro: t("email_set_password_code_intro"),
|
|
||||||
code: sample.code,
|
|
||||||
message: [
|
|
||||||
stripTags(t("email_set_password_code_expiry_html", { minutes: sample.minutes })),
|
|
||||||
t("email_set_password_code_security"),
|
|
||||||
].join("\n\n"),
|
|
||||||
}),
|
|
||||||
preview({
|
|
||||||
id: "account-merged",
|
|
||||||
category: "Аккаунт",
|
|
||||||
title: "Аккаунты объединены",
|
|
||||||
subject: t("email_account_merged_subject"),
|
|
||||||
heading: t("email_account_merged_heading"),
|
|
||||||
intro: t("email_account_merged_intro"),
|
|
||||||
rows: [
|
|
||||||
[t("email_account_merged_row_kept"), "#100200300"],
|
|
||||||
[t("email_account_merged_row_removed"), "#-42"],
|
|
||||||
[t("email_account_merged_row_end_date"), sample.endDate],
|
|
||||||
],
|
|
||||||
message: t("email_account_merged_text", {
|
|
||||||
primary: "#100200300",
|
|
||||||
removed: "#-42",
|
|
||||||
end_date: sample.endDate,
|
|
||||||
}),
|
|
||||||
note: t("email_account_merged_note"),
|
|
||||||
}),
|
|
||||||
paymentPreview({
|
|
||||||
id: "payment-subscription",
|
|
||||||
title: "Оплата подписки",
|
|
||||||
introKey: "email_payment_success_intro_subscription",
|
|
||||||
periodLabel: t("email_payment_success_row_period"),
|
|
||||||
periodValue: t("email_payment_success_period_value", { months: 1 }),
|
|
||||||
textKey: "email_payment_success_text_subscription",
|
|
||||||
values: { months: 1 },
|
|
||||||
}),
|
|
||||||
paymentPreview({
|
|
||||||
id: "payment-traffic",
|
|
||||||
title: "Покупка трафика",
|
|
||||||
introKey: "email_payment_success_intro_traffic",
|
|
||||||
periodLabel: t("email_payment_success_row_traffic"),
|
|
||||||
periodValue: t("email_payment_success_traffic_value", {
|
|
||||||
traffic_gb: sample.regularTraffic,
|
|
||||||
}),
|
|
||||||
textKey: "email_payment_success_text_traffic",
|
|
||||||
values: { traffic_gb: sample.regularTraffic },
|
|
||||||
}),
|
|
||||||
paymentPreview({
|
|
||||||
id: "payment-premium-traffic",
|
|
||||||
title: "Покупка premium-трафика",
|
|
||||||
introKey: "email_payment_success_intro_premium_topup",
|
|
||||||
periodLabel: t("email_payment_success_row_traffic"),
|
|
||||||
periodValue: t("email_payment_success_traffic_value", {
|
|
||||||
traffic_gb: sample.premiumTraffic,
|
|
||||||
}),
|
|
||||||
textKey: "email_payment_success_text_traffic",
|
|
||||||
values: { traffic_gb: sample.premiumTraffic },
|
|
||||||
}),
|
|
||||||
paymentPreview({
|
|
||||||
id: "payment-hwid",
|
|
||||||
title: "Покупка HWID-устройств",
|
|
||||||
introKey: "email_payment_success_intro_hwid",
|
|
||||||
periodLabel: t("email_payment_success_row_hwid"),
|
|
||||||
periodValue: t("email_payment_success_hwid_value", { count: 2 }),
|
|
||||||
textKey: "email_payment_success_text_hwid",
|
|
||||||
values: { count: 2 },
|
|
||||||
}),
|
|
||||||
paymentPreview({
|
|
||||||
id: "payment-tariff-upgrade",
|
|
||||||
title: "Платное повышение тарифа",
|
|
||||||
introKey: "email_payment_success_intro_tariff_upgrade",
|
|
||||||
periodLabel: t("email_payment_success_row_operation"),
|
|
||||||
periodValue: t("email_payment_success_tariff_upgrade_value"),
|
|
||||||
textKey: "email_payment_success_text_tariff_upgrade",
|
|
||||||
}),
|
|
||||||
notificationPreview({
|
|
||||||
id: "payment-failed",
|
|
||||||
title: "Неуспешная оплата",
|
|
||||||
subjectKey: "email_payment_failed_subject",
|
|
||||||
message: "Платёж не был завершён. Можно попробовать ещё раз из личного кабинета.",
|
|
||||||
}),
|
|
||||||
notificationPreview({
|
|
||||||
id: "payment-method-bound",
|
|
||||||
title: "Способ оплаты привязан",
|
|
||||||
subjectKey: "email_payment_method_bound_subject",
|
|
||||||
message: "Автопродление подключено, следующий платёж пройдёт автоматически.",
|
|
||||||
}),
|
|
||||||
notificationPreview({
|
|
||||||
id: "referral-bonus",
|
|
||||||
title: "Реферальный бонус",
|
|
||||||
subjectKey: "email_referral_bonus_subject",
|
|
||||||
message: "Друг активировал подписку, и бонусные дни уже добавлены к вашему аккаунту.",
|
|
||||||
}),
|
|
||||||
notificationPreview({
|
|
||||||
id: "trial-traffic-depleted",
|
|
||||||
title: "Трафик пробного периода закончился",
|
|
||||||
subjectKey: "email_trial_traffic_depleted_subject",
|
|
||||||
message: "Пробный трафик израсходован. Оформите подписку, чтобы продолжить пользоваться сервисом.",
|
|
||||||
}),
|
|
||||||
notificationPreview({
|
|
||||||
id: "regular-traffic-almost",
|
|
||||||
title: "Обычный трафик почти закончился",
|
|
||||||
subjectKey: "email_traffic_warning_regular_almost_subject",
|
|
||||||
ctaKey: "email_traffic_warning_regular_cta",
|
|
||||||
message: "Использовано больше 85% трафика тарифа. Можно докупить пакет заранее.",
|
|
||||||
}),
|
|
||||||
notificationPreview({
|
|
||||||
id: "regular-traffic-depleted",
|
|
||||||
title: "Обычный трафик закончился",
|
|
||||||
subjectKey: "email_traffic_warning_regular_depleted_subject",
|
|
||||||
ctaKey: "email_traffic_warning_regular_cta",
|
|
||||||
message: "Трафик тарифа израсходован. Докупите пакет, чтобы восстановить доступ.",
|
|
||||||
}),
|
|
||||||
notificationPreview({
|
|
||||||
id: "premium-traffic-almost",
|
|
||||||
title: "Premium-трафик почти закончился",
|
|
||||||
subjectKey: "email_traffic_warning_premium_almost_subject",
|
|
||||||
ctaKey: "email_traffic_warning_premium_cta",
|
|
||||||
message: "Premium-трафика осталось мало. Можно докупить пакет до полного расхода.",
|
|
||||||
}),
|
|
||||||
notificationPreview({
|
|
||||||
id: "premium-traffic-depleted",
|
|
||||||
title: "Premium-трафик закончился",
|
|
||||||
subjectKey: "email_traffic_warning_premium_depleted_subject",
|
|
||||||
ctaKey: "email_traffic_warning_premium_cta",
|
|
||||||
message: "Premium-трафик израсходован. Докупите пакет, чтобы продолжить использовать premium-маршруты.",
|
|
||||||
}),
|
|
||||||
expiringPreview({
|
|
||||||
id: "subscription-expiring-today",
|
|
||||||
title: "Подписка заканчивается сегодня",
|
|
||||||
suffix: "today",
|
|
||||||
days: 0,
|
|
||||||
}),
|
|
||||||
expiringPreview({
|
|
||||||
id: "subscription-expiring-tomorrow",
|
|
||||||
title: "Подписка заканчивается завтра",
|
|
||||||
suffix: "tomorrow",
|
|
||||||
days: 1,
|
|
||||||
}),
|
|
||||||
expiringPreview({
|
|
||||||
id: "subscription-expiring-days",
|
|
||||||
title: "Подписка скоро закончится",
|
|
||||||
suffix: "days",
|
|
||||||
days: 3,
|
|
||||||
}),
|
|
||||||
lifecyclePreview({
|
|
||||||
id: "lifecycle-before-days",
|
|
||||||
title: "Lifecycle: осталось несколько дней",
|
|
||||||
subject: lifecycleSubject("email_subscription_lifecycle_subject_before_days", {
|
|
||||||
days: 3,
|
|
||||||
}),
|
|
||||||
message: "Подписка скоро закончится. Продлите её заранее, чтобы доступ не прерывался.",
|
|
||||||
}),
|
|
||||||
lifecyclePreview({
|
|
||||||
id: "lifecycle-before-hours",
|
|
||||||
title: "Lifecycle: осталось несколько часов",
|
|
||||||
subject: lifecycleSubject("email_subscription_lifecycle_subject_before_hours", {
|
|
||||||
hours: 6,
|
|
||||||
}),
|
|
||||||
message: "До окончания подписки осталось несколько часов.",
|
|
||||||
}),
|
|
||||||
lifecyclePreview({
|
|
||||||
id: "lifecycle-expired",
|
|
||||||
title: "Lifecycle: подписка закончилась",
|
|
||||||
subject: lifecycleSubject("email_subscription_lifecycle_subject_expired"),
|
|
||||||
message: "Подписка закончилась. Продлите доступ в личном кабинете.",
|
|
||||||
}),
|
|
||||||
lifecyclePreview({
|
|
||||||
id: "lifecycle-expired-after",
|
|
||||||
title: "Lifecycle: подписка закончилась вчера",
|
|
||||||
subject: lifecycleSubject("email_subscription_lifecycle_subject_expired_after"),
|
|
||||||
message: "Вчера подписка была отключена. Вы можете восстановить доступ продлением.",
|
|
||||||
}),
|
|
||||||
lifecyclePreview({
|
|
||||||
id: "lifecycle-autorenew",
|
|
||||||
title: "Lifecycle: автопродление завтра",
|
|
||||||
subject: lifecycleSubject("email_subscription_lifecycle_subject_autorenew"),
|
|
||||||
message: "Завтра будет выполнено автопродление подписки.",
|
|
||||||
}),
|
|
||||||
lifecyclePreview({
|
|
||||||
id: "lifecycle-mirrored",
|
|
||||||
title: "Lifecycle: копия Telegram-уведомления",
|
|
||||||
subject: lifecycleSubject("email_subscription_lifecycle_subject_before_days", {
|
|
||||||
days: 2,
|
|
||||||
}),
|
|
||||||
introKey: "email_subscription_lifecycle_intro_mirrored",
|
|
||||||
message: "Это письмо дублирует важное уведомление, отправленное в Telegram.",
|
|
||||||
}),
|
|
||||||
preview({
|
|
||||||
id: "support-new-ticket-admin",
|
|
||||||
category: "Поддержка",
|
|
||||||
title: "Новый тикет для администратора",
|
|
||||||
subject: t("email_support_new_ticket_admin_subject", { ticket_id: 42 }),
|
|
||||||
heading: t("email_support_new_ticket_admin_heading", { ticket_id: 42 }),
|
|
||||||
intro: t("email_support_new_ticket_admin_intro"),
|
|
||||||
rows: supportRows(true),
|
|
||||||
message: "Пользователь не может подключиться после продления.",
|
|
||||||
ctaLabel: t("email_support_cta_open_ticket"),
|
|
||||||
ctaUrl: "https://mini.example.com/app/admin/support/42",
|
|
||||||
}),
|
|
||||||
preview({
|
|
||||||
id: "support-user-reply-admin",
|
|
||||||
category: "Поддержка",
|
|
||||||
title: "Ответ пользователя для администратора",
|
|
||||||
subject: t("email_support_user_reply_admin_subject", { ticket_id: 42 }),
|
|
||||||
heading: t("email_support_user_reply_admin_heading", { ticket_id: 42 }),
|
|
||||||
intro: t("email_support_user_reply_admin_intro"),
|
|
||||||
rows: supportRows(true),
|
|
||||||
message: "Проблема повторилась на телефоне и ноутбуке.",
|
|
||||||
ctaLabel: t("email_support_cta_open_ticket"),
|
|
||||||
ctaUrl: "https://mini.example.com/app/admin/support/42",
|
|
||||||
}),
|
|
||||||
preview({
|
|
||||||
id: "support-admin-reply-user",
|
|
||||||
category: "Поддержка",
|
|
||||||
title: "Ответ поддержки пользователю",
|
|
||||||
subject: t("email_support_admin_reply_user_subject", { ticket_id: 42 }),
|
|
||||||
heading: t("email_support_admin_reply_user_heading", { ticket_id: 42 }),
|
|
||||||
intro: t("email_support_admin_reply_user_intro"),
|
|
||||||
rows: supportRows(false),
|
|
||||||
message: "Мы обновили конфигурацию. Попробуйте подключиться ещё раз.",
|
|
||||||
ctaLabel: t("email_support_cta_open_mini_app"),
|
|
||||||
ctaUrl: sample.ticketUrl,
|
|
||||||
}),
|
|
||||||
preview({
|
|
||||||
id: "support-ticket-closed-user",
|
|
||||||
category: "Поддержка",
|
|
||||||
title: "Тикет закрыт",
|
|
||||||
subject: t("email_support_ticket_closed_user_subject", { ticket_id: 42 }),
|
|
||||||
heading: t("email_support_ticket_closed_user_heading", { ticket_id: 42 }),
|
|
||||||
intro: t("email_support_ticket_closed_user_intro"),
|
|
||||||
rows: supportRows(false),
|
|
||||||
message: t("email_support_ticket_closed_user_body"),
|
|
||||||
ctaLabel: t("email_support_cta_open_mini_app"),
|
|
||||||
ctaUrl: sample.ticketUrl,
|
|
||||||
}),
|
|
||||||
];
|
|
||||||
|
|||||||
+23
-139
@@ -288,142 +288,19 @@ const docsHref = '/getting-started/demo/';
|
|||||||
padding: 0.1rem 0 1.6rem;
|
padding: 0.1rem 0 1.6rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mail-card {
|
.email-preview__frame-wrap {
|
||||||
width: min(32rem, 100%);
|
overflow: hidden;
|
||||||
margin: 0 auto;
|
border: 1px solid rgb(148 163 184 / 20%);
|
||||||
padding: 1.35rem 1rem;
|
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
background: #05070a;
|
background: #05070a;
|
||||||
color: #e6e9ef;
|
|
||||||
font-family:
|
|
||||||
Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
|
|
||||||
sans-serif;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mail-card__brand {
|
.email-preview__frame {
|
||||||
margin-bottom: 1rem;
|
|
||||||
color: #00fe7a;
|
|
||||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
|
||||||
font-size: 1.25rem;
|
|
||||||
font-weight: 850;
|
|
||||||
line-height: 1.1;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mail-card__panel {
|
|
||||||
border: 1px solid #1a1f27;
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 1.4rem;
|
|
||||||
background: #0e1116;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mail-card__subject {
|
|
||||||
margin: 0 0 0.7rem;
|
|
||||||
color: #00fe7a;
|
|
||||||
font-size: 0.78rem;
|
|
||||||
font-weight: 750;
|
|
||||||
line-height: 1.35;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mail-card h2 {
|
|
||||||
margin: 0 0 0.7rem;
|
|
||||||
color: #ffffff;
|
|
||||||
font-size: 1.16rem;
|
|
||||||
line-height: 1.25;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mail-card__intro,
|
|
||||||
.mail-card__note,
|
|
||||||
.mail-card__footer {
|
|
||||||
color: #9aa3b2;
|
|
||||||
font-size: 0.86rem;
|
|
||||||
line-height: 1.55;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mail-card__intro {
|
|
||||||
margin: 0 0 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mail-card__note {
|
|
||||||
margin: 0.9rem 0 0;
|
|
||||||
color: #707987;
|
|
||||||
font-size: 0.78rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mail-card__footer {
|
|
||||||
margin: 0.9rem 0 0;
|
|
||||||
color: #5d6573;
|
|
||||||
font-size: 0.72rem;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mail-card__code {
|
|
||||||
margin: 0 0 1rem;
|
|
||||||
border: 1px solid #1a1f27;
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 1rem;
|
|
||||||
background: #05070a;
|
|
||||||
color: #00fe7a;
|
|
||||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
|
||||||
font-size: 2rem;
|
|
||||||
font-weight: 800;
|
|
||||||
line-height: 1;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mail-card__rows {
|
|
||||||
width: 100%;
|
|
||||||
margin: 0 0 1rem;
|
|
||||||
border: 1px solid #1a1f27;
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 0.3rem 0.9rem;
|
|
||||||
background: #05070a;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mail-card__rows td {
|
|
||||||
border-bottom: 1px solid #1a1f27;
|
|
||||||
padding: 0.64rem 0;
|
|
||||||
color: #5d6573;
|
|
||||||
font-size: 0.72rem;
|
|
||||||
font-weight: 750;
|
|
||||||
line-height: 1.2;
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mail-card__rows tr:last-child td {
|
|
||||||
border-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mail-card__rows td:last-child {
|
|
||||||
color: #e6e9ef;
|
|
||||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
|
||||||
font-size: 0.82rem;
|
|
||||||
text-align: right;
|
|
||||||
text-transform: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mail-card__message {
|
|
||||||
margin: 0 0 1rem;
|
|
||||||
border: 1px solid #1a1f27;
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 0.85rem 0.95rem;
|
|
||||||
background: #05070a;
|
|
||||||
color: #e6e9ef;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
line-height: 1.55;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mail-card__cta {
|
|
||||||
display: block;
|
display: block;
|
||||||
border-radius: 8px;
|
width: 100%;
|
||||||
padding: 0.9rem 1rem;
|
height: 760px;
|
||||||
background: #00fe7a;
|
border: 0;
|
||||||
color: #05070a;
|
background: #05070a;
|
||||||
font-size: 0.92rem;
|
|
||||||
font-weight: 800;
|
|
||||||
line-height: 1;
|
|
||||||
text-align: center;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 42rem) {
|
@media (max-width: 42rem) {
|
||||||
@@ -563,12 +440,8 @@ const docsHref = '/getting-started/demo/';
|
|||||||
width: fit-content;
|
width: fit-content;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mail-card__panel {
|
.email-preview__frame {
|
||||||
padding: 1rem;
|
height: 720px;
|
||||||
}
|
|
||||||
|
|
||||||
.mail-card__code {
|
|
||||||
font-size: 1.65rem;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -627,7 +500,8 @@ const docsHref = '/getting-started/demo/';
|
|||||||
<h1 id="email-previews-title">Превью email-писем</h1>
|
<h1 id="email-previews-title">Превью email-писем</h1>
|
||||||
<p>
|
<p>
|
||||||
Все транзакционные письма собраны на одной странице и подписаны по
|
Все транзакционные письма собраны на одной странице и подписаны по
|
||||||
сценарию отправки. Тексты берутся из русской локализации приложения.
|
сценарию отправки. HTML-превью генерируются теми же шаблонами,
|
||||||
|
которые отправляются пользователям.
|
||||||
</p>
|
</p>
|
||||||
</header>
|
</header>
|
||||||
<nav class="email-previews__index" aria-label="Навигация по email-письмам">
|
<nav class="email-previews__index" aria-label="Навигация по email-письмам">
|
||||||
@@ -648,7 +522,17 @@ const docsHref = '/getting-started/demo/';
|
|||||||
</span>
|
</span>
|
||||||
<span class="email-preview__meta">{preview.category}</span>
|
<span class="email-preview__meta">{preview.category}</span>
|
||||||
</summary>
|
</summary>
|
||||||
<div class="email-preview__body" set:html={preview.html} />
|
<div class="email-preview__body">
|
||||||
|
<div class="email-preview__frame-wrap">
|
||||||
|
<iframe
|
||||||
|
class="email-preview__frame"
|
||||||
|
title={`Email preview: ${preview.title}`}
|
||||||
|
srcdoc={preview.html}
|
||||||
|
loading="lazy"
|
||||||
|
sandbox=""
|
||||||
|
></iframe>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</details>
|
</details>
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
|
|
||||||
@@ -64,8 +66,14 @@ def _email_locale_keys_from(path: Path) -> set[str]:
|
|||||||
|
|
||||||
|
|
||||||
def _email_preview_locale_keys() -> set[str]:
|
def _email_preview_locale_keys() -> set[str]:
|
||||||
path = REPO_ROOT / "docs-site" / "src" / "lib" / "emailPreviews.mjs"
|
paths = (
|
||||||
return {match.group("key") for match in EMAIL_KEY_RE.finditer(path.read_text("utf-8"))}
|
REPO_ROOT / "docs-site" / "src" / "lib" / "emailPreviews.mjs",
|
||||||
|
REPO_ROOT / "docs-site" / "scripts" / "generate-email-previews.py",
|
||||||
|
)
|
||||||
|
keys: set[str] = set()
|
||||||
|
for path in paths:
|
||||||
|
keys.update(match.group("key") for match in EMAIL_KEY_RE.finditer(path.read_text("utf-8")))
|
||||||
|
return keys
|
||||||
|
|
||||||
|
|
||||||
def _used_email_locale_keys() -> set[str]:
|
def _used_email_locale_keys() -> set[str]:
|
||||||
@@ -257,3 +265,23 @@ def test_support_email_templates_use_russian_copy_for_russian_recipients():
|
|||||||
"Новый ответ по тикету #42",
|
"Новый ответ по тикету #42",
|
||||||
"Тикет #42 закрыт",
|
"Тикет #42 закрыт",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_docs_email_preview_generator_renders_real_template_html():
|
||||||
|
result = subprocess.run(
|
||||||
|
[sys.executable, str(REPO_ROOT / "docs-site" / "scripts" / "generate-email-previews.py")],
|
||||||
|
cwd=REPO_ROOT,
|
||||||
|
check=True,
|
||||||
|
encoding="utf-8",
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
previews = json.loads(result.stdout)
|
||||||
|
|
||||||
|
assert len(previews) >= 20
|
||||||
|
assert all(preview["html"].lstrip().startswith("<!DOCTYPE html>") for preview in previews)
|
||||||
|
assert all('<html lang="ru"' in preview["html"] for preview in previews)
|
||||||
|
assert all('role="presentation"' in preview["html"] for preview in previews)
|
||||||
|
assert all("mail-card" not in preview["html"] for preview in previews)
|
||||||
|
assert all("email_" not in preview["subject"] + preview["html"] for preview in previews)
|
||||||
|
|||||||
Reference in New Issue
Block a user