docs: add email preview demo section
This commit is contained in:
@@ -11,6 +11,7 @@ const stateMocks = new Set([
|
||||
"devices",
|
||||
"notifications",
|
||||
"auth",
|
||||
"emails",
|
||||
]);
|
||||
const routeMocks = new Set([...stateMocks, "guides", "install"]);
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
@@ -83,6 +84,7 @@ const routeFromParams = () => {
|
||||
let initialRoute = routeFromParams();
|
||||
const mockForRoute = (route) => {
|
||||
const normalized = normalizePath(route);
|
||||
if (normalized === "/emails") return "emails";
|
||||
if (normalized === "/devices") return "devices";
|
||||
if (normalized === "/login" || normalized.startsWith("/login/"))
|
||||
return "auth";
|
||||
@@ -122,7 +124,10 @@ const canonicalizeInitialPublicUrl = (route) => {
|
||||
);
|
||||
};
|
||||
|
||||
if (initialMock === "trial" && initialRoute === "/trial") {
|
||||
if (initialMock === "emails") {
|
||||
initialRoute = "/emails";
|
||||
canonicalizeInitialPublicUrl(initialRoute);
|
||||
} else if (initialMock === "trial" && initialRoute === "/trial") {
|
||||
initialRoute = "/home";
|
||||
const normalizedUrl = new URL(window.location.href);
|
||||
normalizedUrl.pathname = publicPathFromRoute(initialRoute);
|
||||
@@ -134,12 +139,14 @@ if (initialMock === "trial" && initialRoute === "/trial") {
|
||||
} else {
|
||||
canonicalizeInitialPublicUrl(initialRoute);
|
||||
}
|
||||
if (initialMock !== "emails") {
|
||||
params.set("mock", initialMock);
|
||||
params.delete("path");
|
||||
params.delete("screen");
|
||||
params.delete("admin_section");
|
||||
params.set("path", initialRoute);
|
||||
frame.src = `${runtimeBase}/app/?${params.toString()}${window.location.hash || ""}`;
|
||||
}
|
||||
|
||||
const routeFromRuntimeUrl = (url) => {
|
||||
if (url.origin !== window.location.origin) return "";
|
||||
@@ -154,6 +161,7 @@ const routeFromRuntimeUrl = (url) => {
|
||||
return runtimePath;
|
||||
};
|
||||
const routeForStateMock = (mock) => {
|
||||
if (mock === "emails") return "/emails";
|
||||
if (mock === "devices") return "/devices";
|
||||
if (mock === "auth") return "/login";
|
||||
return "/home";
|
||||
@@ -169,8 +177,18 @@ const topbar = document.querySelector(".demo-topbar");
|
||||
const toggle = document.querySelector(".demo-topbar__toggle");
|
||||
const hide = document.querySelector(".demo-topbar__hide");
|
||||
const stateSelect = document.querySelector(".demo-topbar__state-select");
|
||||
const emailPreviews = document.getElementById("email-previews");
|
||||
|
||||
const setDemoMode = (mock) => {
|
||||
const emailMode = mock === "emails";
|
||||
frame.hidden = emailMode;
|
||||
if (emailPreviews) emailPreviews.hidden = !emailMode;
|
||||
if (emailMode) document.body.setAttribute("data-demo-mode", "emails");
|
||||
else document.body.removeAttribute("data-demo-mode");
|
||||
};
|
||||
|
||||
const syncParentUrlFromFrame = () => {
|
||||
if (frame.hidden) return;
|
||||
try {
|
||||
const frameUrl = new URL(frame.contentWindow.location.href);
|
||||
const route = routeFromRuntimeUrl(frameUrl);
|
||||
@@ -208,19 +226,21 @@ const setCollapsed = (collapsed) => {
|
||||
|
||||
toggle?.addEventListener("click", () => setCollapsed(false));
|
||||
hide?.addEventListener("click", () => setCollapsed(true));
|
||||
if (stateSelect) stateSelect.value = normalizeStateMock(params.get("mock"));
|
||||
if (stateSelect) stateSelect.value = normalizeStateMock(params.get("mock") || initialMock);
|
||||
setDemoMode(initialMock);
|
||||
stateSelect?.addEventListener("change", () => {
|
||||
const mock = normalizeStateMock(stateSelect.value);
|
||||
const nextParams = new URLSearchParams(window.location.search);
|
||||
nextParams.delete("path");
|
||||
nextParams.delete("screen");
|
||||
nextParams.delete("admin_section");
|
||||
if (mock === defaultMock) nextParams.delete("mock");
|
||||
if (mock === defaultMock || mock === "emails") nextParams.delete("mock");
|
||||
else nextParams.set("mock", mock);
|
||||
|
||||
const query = nextParams.toString();
|
||||
const stateRoute = routeForStateMock(mock);
|
||||
const publicUrl = `${demoBase}${stateRoute}${query ? `?${query}` : ""}`;
|
||||
window.history.replaceState(null, "", publicUrl);
|
||||
frame.src = runtimeSrc(stateRoute, nextParams);
|
||||
setDemoMode(mock);
|
||||
if (mock !== "emails") frame.src = runtimeSrc(stateRoute, nextParams);
|
||||
});
|
||||
|
||||
@@ -31,6 +31,7 @@ export const demoPublicRouteAliases = ["app"];
|
||||
export const demoPublicRoutes = [
|
||||
...demoPublicRouteAliases,
|
||||
...demoUserRoutes,
|
||||
"emails",
|
||||
"admin",
|
||||
...demoAdminRoutes.map((route) => `admin/${route}`),
|
||||
];
|
||||
|
||||
@@ -0,0 +1,463 @@
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
const localePathCandidates = [
|
||||
resolve(process.cwd(), "..", "locales", "ru.json"),
|
||||
resolve(process.cwd(), "locales", "ru.json"),
|
||||
];
|
||||
const localePath =
|
||||
localePathCandidates.find((candidate) => existsSync(candidate)) ||
|
||||
localePathCandidates[0];
|
||||
const messages = JSON.parse(readFileSync(localePath, "utf8"));
|
||||
|
||||
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 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 escapeHtml = (value) =>
|
||||
String(value || "")
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """);
|
||||
|
||||
const textWithBreaks = (value) => escapeHtml(value).replace(/\n/g, "<br>");
|
||||
|
||||
const stripTags = (value) =>
|
||||
String(value || "")
|
||||
.replace(/<br\s*\/?>/gi, "\n")
|
||||
.replace(/<[^>]*>/g, "");
|
||||
|
||||
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)}
|
||||
${
|
||||
message
|
||||
? `<div class="mail-card__message">${textWithBreaks(message)}</div>`
|
||||
: ""
|
||||
}
|
||||
${
|
||||
ctaLabel
|
||||
? `<a class="mail-card__cta" href="${escapeHtml(ctaUrl || sample.dashboardUrl)}">${escapeHtml(
|
||||
ctaLabel,
|
||||
)}</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) => ({
|
||||
...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,
|
||||
}),
|
||||
];
|
||||
@@ -1,4 +1,6 @@
|
||||
---
|
||||
import { emailPreviews } from '../lib/emailPreviews.mjs';
|
||||
|
||||
const defaultDemoSrc = '/demo/runtime/app/?path=/home&mock=tariffs';
|
||||
const docsHref = '/getting-started/demo/';
|
||||
---
|
||||
@@ -44,6 +46,10 @@ const docsHref = '/getting-started/demo/';
|
||||
background: #05080f;
|
||||
}
|
||||
|
||||
body[data-demo-mode='emails'] {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.demo-topbar {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
@@ -151,6 +157,275 @@ const docsHref = '/getting-started/demo/';
|
||||
background: #05080f;
|
||||
}
|
||||
|
||||
.demo-frame[hidden],
|
||||
.email-previews[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.email-previews {
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
background:
|
||||
linear-gradient(180deg, rgb(15 23 42 / 32%), rgb(5 8 15 / 0) 14rem),
|
||||
#05080f;
|
||||
}
|
||||
|
||||
.email-previews__inner {
|
||||
width: min(72rem, calc(100% - 2rem));
|
||||
margin: 0 auto;
|
||||
padding: 2rem 0 4rem;
|
||||
}
|
||||
|
||||
.email-previews__header {
|
||||
display: grid;
|
||||
gap: 0.45rem;
|
||||
max-width: 44rem;
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
.email-previews__eyebrow {
|
||||
color: #00fe7a;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 800;
|
||||
line-height: 1;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.email-previews__header h1 {
|
||||
margin: 0;
|
||||
color: #ffffff;
|
||||
font-size: 1.65rem;
|
||||
line-height: 1.15;
|
||||
}
|
||||
|
||||
.email-previews__header p {
|
||||
margin: 0;
|
||||
color: #a7b1c2;
|
||||
font-size: 0.95rem;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.email-previews__index {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.45rem;
|
||||
margin: 0 0 1.4rem;
|
||||
}
|
||||
|
||||
.email-previews__index a {
|
||||
border: 1px solid rgb(148 163 184 / 24%);
|
||||
border-radius: 7px;
|
||||
padding: 0.42rem 0.6rem;
|
||||
background: rgb(15 23 42 / 62%);
|
||||
color: #dbeafe;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 650;
|
||||
line-height: 1;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.email-previews__index a:hover {
|
||||
border-color: rgb(0 254 122 / 54%);
|
||||
color: #00fe7a;
|
||||
}
|
||||
|
||||
.email-preview {
|
||||
border-top: 1px solid rgb(148 163 184 / 18%);
|
||||
}
|
||||
|
||||
.email-preview:last-child {
|
||||
border-bottom: 1px solid rgb(148 163 184 / 18%);
|
||||
}
|
||||
|
||||
.email-preview summary {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
padding: 1rem 0;
|
||||
cursor: pointer;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.email-preview summary::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.email-preview__title {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.email-preview__title strong {
|
||||
display: block;
|
||||
color: #f8fafc;
|
||||
font-size: 0.98rem;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.email-preview__title span {
|
||||
display: block;
|
||||
margin-top: 0.22rem;
|
||||
color: #94a3b8;
|
||||
font-size: 0.78rem;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.email-preview__meta {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px solid rgb(14 165 233 / 34%);
|
||||
border-radius: 999px;
|
||||
padding: 0.28rem 0.55rem;
|
||||
color: #bae6fd;
|
||||
font-size: 0.72rem;
|
||||
font-weight: 750;
|
||||
line-height: 1;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.email-preview__body {
|
||||
padding: 0.1rem 0 1.6rem;
|
||||
}
|
||||
|
||||
.mail-card {
|
||||
width: min(32rem, 100%);
|
||||
margin: 0 auto;
|
||||
padding: 1.35rem 1rem;
|
||||
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 {
|
||||
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;
|
||||
}
|
||||
|
||||
@media (max-width: 42rem) {
|
||||
body {
|
||||
display: block;
|
||||
@@ -269,6 +544,32 @@ const docsHref = '/getting-started/demo/';
|
||||
.demo-frame {
|
||||
height: 100dvh;
|
||||
}
|
||||
|
||||
.email-previews {
|
||||
min-height: 100dvh;
|
||||
}
|
||||
|
||||
.email-previews__inner {
|
||||
width: min(calc(100% - 1rem), 72rem);
|
||||
padding-top: 4rem;
|
||||
}
|
||||
|
||||
.email-preview summary {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
gap: 0.55rem;
|
||||
}
|
||||
|
||||
.email-preview__meta {
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.mail-card__panel {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.mail-card__code {
|
||||
font-size: 1.65rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
@@ -296,6 +597,7 @@ const docsHref = '/getting-started/demo/';
|
||||
<option value="devices">Лимит и докупка устройств</option>
|
||||
<option value="notifications">Telegram-уведомления</option>
|
||||
<option value="auth">Вход и регистрация</option>
|
||||
<option value="emails">Email-письма</option>
|
||||
</select>
|
||||
</label>
|
||||
<div class="demo-topbar__actions">
|
||||
@@ -313,6 +615,46 @@ const docsHref = '/getting-started/demo/';
|
||||
src={defaultDemoSrc}
|
||||
loading="eager"
|
||||
></iframe>
|
||||
<section
|
||||
id="email-previews"
|
||||
class="email-previews"
|
||||
aria-labelledby="email-previews-title"
|
||||
hidden
|
||||
>
|
||||
<div class="email-previews__inner">
|
||||
<header class="email-previews__header">
|
||||
<div class="email-previews__eyebrow">Email preview</div>
|
||||
<h1 id="email-previews-title">Превью email-писем</h1>
|
||||
<p>
|
||||
Все транзакционные письма собраны на одной странице и подписаны по
|
||||
сценарию отправки. Тексты берутся из русской локализации приложения.
|
||||
</p>
|
||||
</header>
|
||||
<nav class="email-previews__index" aria-label="Навигация по email-письмам">
|
||||
{
|
||||
emailPreviews.map((preview) => (
|
||||
<a href={`#${preview.id}`}>{preview.title}</a>
|
||||
))
|
||||
}
|
||||
</nav>
|
||||
<div class="email-previews__list">
|
||||
{
|
||||
emailPreviews.map((preview, index) => (
|
||||
<details class="email-preview" id={preview.id} open={index === 0}>
|
||||
<summary>
|
||||
<span class="email-preview__title">
|
||||
<strong>{preview.title}</strong>
|
||||
<span>{preview.subject}</span>
|
||||
</span>
|
||||
<span class="email-preview__meta">{preview.category}</span>
|
||||
</summary>
|
||||
<div class="email-preview__body" set:html={preview.html} />
|
||||
</details>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<script is:inline src="/demo/demo-shell.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user