From df8f2636d2c554ace386240c3ba4328ff586d06c Mon Sep 17 00:00:00 2001 From: 3252a8 <3252a8@proton.me> Date: Sun, 31 May 2026 15:08:22 +0300 Subject: [PATCH] docs: render email previews from templates --- docs-site/scripts/generate-email-previews.py | 406 +++++++++++++++ docs-site/src/lib/emailPreviews.mjs | 504 ++----------------- docs-site/src/pages/demo.astro | 162 +----- tests/test_email_localization.py | 32 +- 4 files changed, 507 insertions(+), 597 deletions(-) create mode 100644 docs-site/scripts/generate-email-previews.py diff --git a/docs-site/scripts/generate-email-previews.py b/docs-site/scripts/generate-email-previews.py new file mode 100644 index 0000000..e3a4253 --- /dev/null +++ b/docs-site/scripts/generate-email-previews.py @@ -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)) diff --git a/docs-site/src/lib/emailPreviews.mjs b/docs-site/src/lib/emailPreviews.mjs index e18b388..3a0d6b8 100644 --- a/docs-site/src/lib/emailPreviews.mjs +++ b/docs-site/src/lib/emailPreviews.mjs @@ -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"; -const localePathCandidates = [ - resolve(process.cwd(), "..", "locales", "ru.json"), - resolve(process.cwd(), "locales", "ru.json"), +const repoRootCandidates = [ + resolve(process.cwd(), ".."), + resolve(process.cwd()), ]; -const localePath = - localePathCandidates.find((candidate) => existsSync(candidate)) || - localePathCandidates[0]; -const messages = JSON.parse(readFileSync(localePath, "utf8")); +const repoRoot = + repoRootCandidates.find( + (candidate) => + existsSync(resolve(candidate, "backend")) && + existsSync(resolve(candidate, "docs-site")), + ) || repoRootCandidates[0]; +const generatorPath = resolve( + repoRoot, + "docs-site", + "scripts", + "generate-email-previews.py", +); -const sample = { - amount: "390 RUB", - brand: "remnawave-minishop", - code: "483921", - dashboardUrl: "https://mini.example.com/app", - endDate: "21.06.2026, 18:00", - magicUrl: "https://mini.example.com/app/auth/magic/preview", - minutes: 10, - premiumTraffic: "25", - regularTraffic: "100", - ticketUrl: "https://mini.example.com/app/support/42", -}; +const pythonCommands = [ + process.env.PYTHON, + process.platform === "win32" ? "python" : "python3", + "python", +].filter(Boolean); -const interpolate = (template, values = {}) => - String(template || "").replace(/\{([a-zA-Z0-9_]+)\}/g, (match, name) => - Object.prototype.hasOwnProperty.call(values, name) ? values[name] : match, +let lastError = ""; +let generated = null; +for (const command of pythonCommands) { + const result = spawnSync(command, [generatorPath], { + cwd: repoRoot, + encoding: "utf8", + env: { + ...process.env, + PYTHONIOENCODING: "utf-8", + }, + }); + if (result.status === 0 && result.stdout) { + generated = result.stdout; + break; + } + lastError = [result.error?.message, result.stderr, result.stdout] + .filter(Boolean) + .join("\n") + .trim(); +} + +if (!generated) { + throw new Error( + `Failed to generate email previews from backend templates.\n${lastError}`, ); +} -const t = (key, values = {}) => interpolate(messages[key] || key, values); - -const escapeHtml = (value) => - String(value || "") - .replace(/&/g, "&") - .replace(//g, ">") - .replace(/"/g, """); - -const textWithBreaks = (value) => escapeHtml(value).replace(/\n/g, "
"); - -const stripTags = (value) => - String(value || "") - .replace(//gi, "\n") - .replace(/<[^>]*>/g, ""); - -const renderRows = (rows = []) => { - if (!rows.length) return ""; - return ` - ${rows - .map( - ([label, value]) => ` - - - `, - ) - .join("")} - `; -}; - -const renderEmailHtml = ({ - subject, - heading = subject, - intro, - rows, - code, - message, - note, - ctaLabel, - ctaUrl, -}) => `
-
${escapeHtml(sample.brand)}
-
-

${escapeHtml(subject)}

-

${escapeHtml(heading)}

- ${intro ? `

${textWithBreaks(intro)}

` : ""} - ${ - code - ? `
${escapeHtml(code)}
` - : "" - } - ${renderRows(rows)} - ${ - message - ? `
${textWithBreaks(message)}
` - : "" - } - ${ - ctaLabel - ? `${escapeHtml( - ctaLabel, - )}` - : "" - } - ${note ? `

${textWithBreaks(note)}

` : ""} -
- -
`; - -const preview = (item) => ({ - ...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, - }), -]; +export const emailPreviews = JSON.parse(generated); diff --git a/docs-site/src/pages/demo.astro b/docs-site/src/pages/demo.astro index 9028d85..f5ce8e4 100644 --- a/docs-site/src/pages/demo.astro +++ b/docs-site/src/pages/demo.astro @@ -288,142 +288,19 @@ const docsHref = '/getting-started/demo/'; padding: 0.1rem 0 1.6rem; } - .mail-card { - width: min(32rem, 100%); - margin: 0 auto; - padding: 1.35rem 1rem; + .email-preview__frame-wrap { + overflow: hidden; + border: 1px solid rgb(148 163 184 / 20%); border-radius: 8px; background: #05070a; - color: #e6e9ef; - font-family: - Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", - sans-serif; } - .mail-card__brand { - 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 { + .email-preview__frame { display: block; - border-radius: 8px; - padding: 0.9rem 1rem; - background: #00fe7a; - color: #05070a; - font-size: 0.92rem; - font-weight: 800; - line-height: 1; - text-align: center; - text-decoration: none; + width: 100%; + height: 760px; + border: 0; + background: #05070a; } @media (max-width: 42rem) { @@ -563,12 +440,8 @@ const docsHref = '/getting-started/demo/'; width: fit-content; } - .mail-card__panel { - padding: 1rem; - } - - .mail-card__code { - font-size: 1.65rem; + .email-preview__frame { + height: 720px; } } @@ -627,7 +500,8 @@ const docsHref = '/getting-started/demo/';

Превью email-писем

Все транзакционные письма собраны на одной странице и подписаны по - сценарию отправки. Тексты берутся из русской локализации приложения. + сценарию отправки. HTML-превью генерируются теми же шаблонами, + которые отправляются пользователям.