feat: tune webapp visual

This commit is contained in:
3252a8
2026-04-30 00:06:31 +03:00
parent 4ce78f277e
commit 2df4d2d770
15 changed files with 2092 additions and 16875 deletions
+1
View File
@@ -28,6 +28,7 @@ tmp/
# WebApp build artifacts (regenerated inside Docker)
bot/app/web/templates/subscription_webapp.css
bot/app/web/templates/subscription_webapp.js
bot/app/web/templates/subscription_webapp.min.*.js
# Byte-compiled / optimized / DLL files
+1
View File
@@ -12,6 +12,7 @@ node_modules/
# WebApp build artifacts (regenerated by `npm run build:webapp` / Docker build)
bot/app/web/templates/subscription_webapp.css
bot/app/web/templates/subscription_webapp.js
bot/app/web/templates/subscription_webapp.min.*.js
tmp
.claude
+3
View File
@@ -37,9 +37,12 @@ COPY . .
# Replace template assets with freshly built ones
RUN rm -f bot/app/web/templates/subscription_webapp.css \
bot/app/web/templates/subscription_webapp.js \
bot/app/web/templates/subscription_webapp.min.*.js
COPY --from=webapp-builder /webapp/bot/app/web/templates/subscription_webapp.css \
bot/app/web/templates/subscription_webapp.css
COPY --from=webapp-builder /webapp/bot/app/web/templates/subscription_webapp.js \
bot/app/web/templates/subscription_webapp.js
COPY --from=webapp-builder /webapp/bot/app/web/templates/subscription_webapp.min.*.js \
bot/app/web/templates/
File diff suppressed because it is too large Load Diff
+4 -3
View File
@@ -197,11 +197,12 @@
</div>
<Card class="auth-card">
<div class="field-label">Вход по email</div>
<div class="email-row"><div class="input muted">Email</div><Button variant="outline"><Mail size={17} />Получить код</Button></div>
<div class="auth-email-stack"><div class="input muted">Email</div><Button class="wide"><Mail size={17} />Войти по почте</Button></div>
<div class="or-line"><span></span>или<span></span></div>
<Button variant="telegram" class="wide"><Send size={18} />Войти через Telegram</Button>
<Button variant="telegram" class="wide telegram-login-button">
<span class="telegram-login-text"><Send size={17} />Войти через телеграм</span>
</Button>
</Card>
<div class="auth-bottom">Нет аккаунта? <strong>Создать</strong></div>
</PhoneFrame>
<PhoneFrame number="9" label="Подтверждение по коду" wide>
@@ -6,6 +6,7 @@
export let open = false;
export let title = "";
export let description = "";
export let closeLabel = "Close";
export let onclose = () => {};
let className = "";
export { className as class };
@@ -13,14 +14,14 @@
{#if open}
<div class="dialog" role="dialog" aria-modal="true" aria-label={title}>
<button class="dialog-backdrop" type="button" aria-label="Закрыть" on:click={onclose}></button>
<button class="dialog-backdrop" type="button" aria-label={closeLabel} on:click={onclose}></button>
<section class={cn("dialog-card", className)}>
<div class="dialog-head">
<div>
{#if title}<h2>{title}</h2>{/if}
{#if description}<p>{description}</p>{/if}
</div>
<Button variant="icon" size="icon" onclick={onclose} aria-label="Закрыть">
<Button variant="icon" size="icon" onclick={onclose} aria-label={closeLabel}>
<X size={18} />
</Button>
</div>
@@ -15,10 +15,15 @@
<input
bind:value
class={cn("input", className)}
on:keydown
on:input
on:focus
on:blur
{type}
{placeholder}
{inputmode}
{maxlength}
{autocomplete}
{disabled}
{...$$restProps}
/>
File diff suppressed because it is too large Load Diff
+53 -4
View File
@@ -103,6 +103,12 @@ class WebAppPaymentCreatePayload(BaseModel):
comment: Optional[constr(max_length=4096)] = None
note: Optional[constr(max_length=4096)] = None
class WebAppLanguagePayload(BaseModel):
model_config = ConfigDict(extra="ignore")
language: constr(min_length=2, max_length=16)
_SHARED_HTTP_SESSION: Optional[ClientSession] = None
_SHARED_HTTP_SESSION_LOCK = asyncio.Lock()
@@ -157,6 +163,9 @@ def create_subscription_webapp_application(
def setup_subscription_webapp_routes(app: web.Application) -> None:
app.router.add_get("/", index_route)
app.router.add_get("/home", index_route)
app.router.add_get("/invite", index_route)
app.router.add_get("/settings", index_route)
app.router.add_get("/health", health_route)
app.router.add_get(WEBAPP_LOGO_PROXY_PATH, webapp_logo_route)
app.router.add_get("/subscription_webapp.css", css_asset_route)
@@ -168,6 +177,7 @@ def setup_subscription_webapp_routes(app: web.Application) -> None:
app.router.add_post("/api/auth/email/magic", email_auth_magic_route)
app.router.add_post("/api/auth/logout", logout_route)
app.router.add_get("/api/me", me_route)
app.router.add_post("/api/account/language", account_language_route)
app.router.add_post("/api/account/email/request", account_email_request_route)
app.router.add_post("/api/account/email/verify", account_email_verify_route)
app.router.add_post("/api/account/telegram/link", account_telegram_link_route)
@@ -190,11 +200,23 @@ def _resolve_webapp_logo_url(settings: Settings) -> str:
return ""
parsed_logo_url = urlsplit(raw_logo_url)
if parsed_logo_url.scheme == "https" and parsed_logo_url.hostname:
return WEBAPP_LOGO_PROXY_PATH
if parsed_logo_url.scheme in {"https", "http", "data"}:
return raw_logo_url
if raw_logo_url.startswith("/"):
return raw_logo_url
return ""
def _resolve_telegram_bot_id(bot_token: str) -> Optional[int]:
token_prefix = str(bot_token or "").strip().split(":", 1)[0]
if not token_prefix.isdigit():
return None
try:
return int(token_prefix)
except ValueError:
return None
async def webapp_logo_route(request: web.Request) -> web.Response:
settings: Settings = request.app["settings"]
raw_logo_url = (settings.WEBAPP_LOGO_URL or "").strip()
@@ -353,7 +375,7 @@ async def _security_headers_middleware(request: web.Request, handler):
"frame-ancestors https://web.telegram.org https://t.me; "
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; "
"font-src 'self' https://fonts.gstatic.com https://cdn.jsdelivr.net data:; "
"img-src 'self' data: https:; "
"img-src 'self' data: https: http:; "
"connect-src 'self'; "
"object-src 'none'; "
"base-uri 'self'; "
@@ -492,6 +514,7 @@ async def index_route(request: web.Request) -> web.Response:
"logoEmoji": settings.WEBAPP_LOGO_EMOJI,
"apiBase": "/api",
"telegramLoginBotUsername": request.app.get("bot_username") or "",
"telegramLoginBotId": _resolve_telegram_bot_id(settings.BOT_TOKEN) or 0,
"supportUrl": cached["support_url"],
"termsUrl": cached["terms_url"],
"privacyPolicyUrl": cached["privacy_policy_url"],
@@ -522,7 +545,7 @@ async def index_route(request: web.Request) -> web.Response:
)
html = html.replace(
WEBAPP_JS_PLACEHOLDER,
f'<script src="./{_resolve_webapp_js_asset_name()}" defer></script>',
f'<script src="/{_resolve_webapp_js_asset_name()}" defer></script>',
)
return web.Response(text=html, content_type="text/html", charset="utf-8")
@@ -1247,6 +1270,29 @@ async def me_route(request: web.Request) -> web.Response:
return web.json_response({"ok": True, **data})
async def account_language_route(request: web.Request) -> web.Response:
user_id = _require_user_id(request)
payload = await _read_json(request)
language_payload, validation_error = _validate_model_payload(WebAppLanguagePayload, payload)
if validation_error:
return validation_error
language = _normalize_language(str(language_payload.language or ""))
async_session_factory: sessionmaker = request.app["async_session_factory"]
async with async_session_factory() as session:
db_user = await user_dal.get_user_by_id(session, user_id)
if not db_user or db_user.is_banned:
await session.rollback()
return _json_error(403, "access_denied", "Access denied")
if _normalize_language(db_user.language_code or "") != language:
db_user.language_code = language
await session.flush()
await session.commit()
return web.json_response({"ok": True, "language": language})
async def apply_promo_route(request: web.Request) -> web.Response:
user_id = _require_user_id(request)
payload = await _read_json(request)
@@ -1963,6 +2009,8 @@ async def _build_user_payload(request: web.Request, user_id: int) -> Dict[str, A
"webapp_link": webapp_referral_link,
"invited_count": referral_stats.get("invited_count", 0),
"purchased_count": referral_stats.get("purchased_count", 0),
"welcome_bonus_days": max(0, int(getattr(settings, "REFERRAL_WELCOME_BONUS_DAYS", 0) or 0)),
"one_bonus_per_referee": bool(getattr(settings, "REFERRAL_ONE_BONUS_PER_REFEREE", False)),
"bonus_details": _serialize_referral_bonus_details(settings, lang),
},
"plans": _serialize_plans(
@@ -2060,6 +2108,7 @@ def _serialize_subscription(
"traffic_used": _format_bytes(active.get("traffic_used_bytes")),
"traffic_limit_bytes": _coerce_int_or_none(active.get("traffic_limit_bytes")),
"traffic_used_bytes": _coerce_int_or_none(active.get("traffic_used_bytes")),
"traffic_limit_strategy": str(active.get("traffic_limit_strategy") or ""),
"auto_renew_enabled": bool(getattr(local_sub, "auto_renew_enabled", False)),
"provider": getattr(local_sub, "provider", None),
}
File diff suppressed because it is too large Load Diff
@@ -12,7 +12,7 @@
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
<script src="https://telegram.org/js/telegram-web-app.js"></script>
<link rel="stylesheet" href="./subscription_webapp.css">
<link rel="stylesheet" href="/subscription_webapp.css">
</head>
<body>
@@ -22,7 +22,7 @@
<!-- WEBAPP_CONFIG_SCRIPT -->
<!-- WEBAPP_JS_SCRIPT -->
<!-- WEBAPP_DEV_MOCK_START -->
<script src="./subscription_webapp.js" defer></script>
<script src="/subscription_webapp.js" defer></script>
<!-- WEBAPP_DEV_MOCK_END -->
</body>
File diff suppressed because it is too large Load Diff
@@ -1,916 +0,0 @@
@import "tailwindcss";
@source "./subscription_webapp.html";
@source "./subscription_webapp.js";
:root {
color-scheme: dark;
--bg-primary: #05070a;
--bg-card: #0f1521;
--bg-card-hover: #161e2e;
--border: #1e2940;
--text-primary: #e2e8f0;
--text-secondary: #94a3b8;
--text-muted: #64748b;
--accent: #00fe7a;
--accent-hover: #5aff9f;
--success: #10b981;
--danger: #ef4444;
--font-sans: Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
--font-mono: "JetBrains Mono", "Fira Code", monospace;
--radius-sm: 6px;
--radius-md: 8px;
--radius-lg: 8px;
--transition: 0.2s cubic-bezier(0.4, 0, 0.2, 1);
--app-safe-top-extra: 10px;
--app-safe-top: max(var(--tg-content-safe-area-inset-top, 0px), var(--tg-safe-area-inset-top, 0px), env(safe-area-inset-top));
--app-safe-bottom: max(var(--tg-content-safe-area-inset-bottom, 0px), var(--tg-safe-area-inset-bottom, 0px), env(safe-area-inset-bottom));
}
@layer base {
html,
body {
@apply m-0 min-h-full w-full overflow-x-hidden tracking-[0];
}
body {
@apply overflow-x-hidden bg-[var(--bg-primary)] font-[family-name:var(--font-sans)] text-[var(--text-primary)] antialiased;
-webkit-tap-highlight-color: transparent;
}
body.modal-open {
@apply overflow-hidden;
}
button,
a {
font: inherit;
}
button {
@apply cursor-pointer border-0;
}
button:disabled {
@apply cursor-progress opacity-[0.58];
}
}
@layer components {
.btn-sm {
@apply inline-flex min-h-[38px] min-w-0 cursor-pointer items-center justify-center gap-2 rounded-[var(--radius-md)] border border-[var(--border)] bg-[rgba(255,255,255,0.03)] px-[11px] py-2 text-center font-extrabold text-[13px] leading-none text-[var(--text-secondary)] no-underline transition-[transform,border-color,background,color,box-shadow] duration-200 ease-[cubic-bezier(0.4,0,0.2,1)] hover:-translate-y-0.5 hover:border-[color-mix(in_srgb,var(--accent)_42%,var(--border))] hover:bg-[var(--bg-card-hover)];
}
.metrics {
@apply grid gap-0 overflow-hidden rounded-[var(--radius-md)] border border-[var(--border)] bg-[rgba(255,255,255,0.018)];
}
.metric {
@apply grid min-h-12 grid-cols-[minmax(0,0.88fr)_minmax(0,1.12fr)] items-center gap-2.5 border border-[var(--border)] bg-[rgba(255,255,255,0.02)] px-3 py-2.5;
}
.auth-tab {
@apply min-h-9 rounded-[var(--radius-sm)] bg-transparent font-[850] text-[var(--text-secondary)] transition-colors;
}
.login-layout {
@apply hidden mx-auto min-h-screen w-[min(100vw,560px)] flex-col items-center justify-center gap-4 px-[14px];
padding-top: calc(max(var(--app-safe-top), 14px) + var(--app-safe-top-extra));
padding-bottom: max(var(--app-safe-bottom), 18px);
}
.section-label {
@apply font-[family-name:var(--font-mono)] text-[11px] font-extrabold text-[var(--text-muted)];
}
.app-loader {
@apply grid min-h-[78vh] place-items-center font-[family-name:var(--font-mono)] font-extrabold text-[var(--text-muted)];
}
.auth-tabs-wrapper {
@apply grid min-h-[46px] grid-cols-2 gap-1 rounded-[var(--radius-md)] border border-[var(--border)] bg-[rgba(5,8,13,0.38)] p-1;
}
.panel-head {
@apply flex items-start justify-between gap-3;
}
.section-title {
@apply text-lg font-extrabold leading-tight text-[var(--text-primary)];
}
.section-caption {
@apply mt-[3px] text-[13px] text-[var(--text-secondary)];
}
.metric-label {
@apply min-w-0 font-[family-name:var(--font-mono)] text-[11px] font-bold text-[var(--text-muted)];
}
.metric-value {
@apply min-w-0 text-right text-sm font-bold text-[var(--text-primary)];
}
.referral-link-row {
@apply grid min-h-[58px] grid-cols-[minmax(0,1fr)_48px] items-center gap-2.5 rounded-[var(--radius-md)] border border-[var(--border)] bg-[rgba(255,255,255,0.02)] px-3 py-[11px];
}
.referral-link-value {
@apply mt-1 font-[family-name:var(--font-mono)] text-xs font-bold leading-[1.35] text-[var(--text-primary)];
}
.bonus-row {
@apply grid min-h-[58px] grid-cols-[minmax(0,0.8fr)_minmax(0,1.2fr)] items-center gap-2.5 rounded-[var(--radius-md)] border border-[var(--border)] bg-[rgba(255,255,255,0.02)] px-3 py-[11px];
}
.empty-state {
@apply rounded-[var(--radius-md)] border border-[var(--border)] bg-[rgba(255,255,255,0.02)] p-[13px] text-sm leading-[1.45] text-[var(--text-secondary)];
}
.plan-card {
@apply inline-flex min-h-[44px] w-full min-w-0 items-center justify-center rounded-[var(--radius-md)] border border-[var(--border)] bg-[rgba(255,255,255,0.02)] px-3 py-2 text-center font-extrabold text-[14px] leading-tight text-[var(--text-primary)] transition-[transform,border-color,background,box-shadow] hover:-translate-y-0.5 hover:border-[color-mix(in_srgb,var(--accent)_42%,var(--border))] hover:bg-[var(--bg-card-hover)];
transition-duration: 200ms;
}
.plan-card-active {
@apply border-[var(--accent)] bg-[color-mix(in_srgb,var(--accent)_8%,transparent)] text-[var(--accent)] ring-1 ring-[color-mix(in_srgb,var(--accent)_58%,transparent)] shadow-[0_12px_30px_rgba(0,0,0,0.2)];
}
.payment-method-card {
@apply inline-flex min-h-[52px] w-full min-w-0 items-center justify-center rounded-[var(--radius-md)] border border-[var(--border)] bg-[rgba(255,255,255,0.03)] px-4 py-[12px] text-center font-extrabold text-[15px] leading-tight text-[var(--text-primary)] transition-[transform,border-color,background,box-shadow] hover:-translate-y-0.5 hover:border-[color-mix(in_srgb,var(--accent)_42%,var(--border))] hover:bg-[var(--bg-card-hover)];
transition-duration: 200ms;
}
.payment-method-card--platega {
border-color: rgba(0, 254, 122, 0.36);
background: rgba(0, 254, 122, 0.08);
}
.payment-method-card--cryptopay {
border-color: rgba(34, 211, 238, 0.36);
background: rgba(34, 211, 238, 0.08);
}
.plan-name {
@apply block text-[15px] font-extrabold leading-tight;
}
.plan-meta {
@apply mt-1 block font-[family-name:var(--font-mono)] text-[11px] font-bold leading-[1.3] text-[var(--text-muted)];
}
.plan-price {
@apply block max-w-[48%] flex-none text-right font-[family-name:var(--font-mono)] text-[15px] font-extrabold leading-tight text-[var(--accent)];
}
.notice {
@apply rounded-[var(--radius-md)] border border-[var(--border)] bg-[rgba(255,255,255,0.02)] p-[13px] text-sm leading-[1.45] text-[var(--text-secondary)];
}
.step-num {
@apply block font-[family-name:var(--font-mono)] text-[11px] font-extrabold leading-[1.1] text-current;
}
.step-name {
@apply mt-1 block text-xs font-extrabold leading-[1.15] text-current;
}
.main-value {
@apply m-0 text-[32px] font-extrabold leading-[1.08] text-[var(--text-primary)];
}
.main-caption {
@apply mt-1.5 mb-0 text-sm leading-[1.45] text-[var(--text-secondary)];
}
.status-text {
@apply m-0 min-h-5 text-sm leading-[1.45] text-[var(--text-secondary)];
}
.legal-links {
@apply flex flex-wrap justify-center gap-x-4 gap-y-1.5 text-center;
}
.app-header {
@apply flex min-h-12 items-center justify-between gap-3;
}
.app-header-title {
@apply flex min-w-0 items-center gap-2.5;
}
.app-header-actions {
@apply flex min-w-0 items-center gap-2;
}
.panel-head-sm {
@apply flex min-w-0 items-center justify-between gap-2.5;
}
.user-menu {
@apply relative inline-flex;
}
.lang-menu {
@apply relative inline-flex;
}
.lang-chip {
@apply inline-flex min-h-[38px] cursor-pointer items-center gap-1.5 rounded-[var(--radius-md)] border border-[var(--border)] bg-[rgba(255,255,255,0.03)] px-2 text-left font-extrabold text-[12px] text-[var(--text-primary)] no-underline transition-[transform,border-color,background,color,box-shadow] duration-200 ease-[cubic-bezier(0.4,0,0.2,1)] hover:border-[color-mix(in_srgb,var(--accent)_42%,var(--border))] hover:bg-[var(--bg-card-hover)];
}
.lang-chip[aria-expanded="true"] {
@apply border-[color-mix(in_srgb,var(--accent)_55%,var(--border))] bg-[var(--bg-card-hover)];
}
.lang-chip-flag {
@apply text-[14px] leading-none;
font-family: 'Twemoji Country Flags', 'Apple Color Emoji', 'Segoe UI Emoji', 'Noto Color Emoji', sans-serif;
}
.lang-chip-label {
@apply font-[family-name:var(--font-mono)] text-[11px] tracking-wide;
}
.lang-chip-caret {
@apply h-3 w-3 flex-none fill-[var(--text-secondary)] transition-transform;
}
.lang-chip[aria-expanded="true"] .lang-chip-caret {
transform: rotate(180deg);
}
.lang-dropdown {
@apply absolute right-0 top-[calc(100%+8px)] z-30 grid w-[160px] origin-top-right gap-0 rounded-[var(--radius-md)] border border-[var(--border)] bg-[var(--bg-card)] opacity-0 shadow-[0_24px_48px_rgba(0,0,0,0.45)] transition-[opacity,transform] duration-200 ease-[cubic-bezier(0.2,0.8,0.2,1)] pointer-events-none -translate-y-2 scale-[0.96];
backdrop-filter: blur(14px);
will-change: opacity, transform;
}
.lang-dropdown.show {
@apply opacity-100 pointer-events-auto translate-y-0 scale-100;
}
.lang-dropdown-item {
@apply flex cursor-pointer items-center gap-2.5 border-0 bg-transparent px-3 py-2.5 text-left text-[13px] font-bold text-[var(--text-secondary)] transition-colors hover:bg-[var(--bg-card-hover)] hover:text-[var(--text-primary)];
}
.lang-dropdown-item + .lang-dropdown-item {
@apply border-t border-[var(--border)];
}
.lang-dropdown-item.is-active {
@apply bg-[var(--bg-card-hover)] text-[var(--text-primary)];
}
.lang-dropdown-flag {
@apply text-[16px] leading-none;
font-family: 'Twemoji Country Flags', 'Apple Color Emoji', 'Segoe UI Emoji', 'Noto Color Emoji', sans-serif;
}
.lang-dropdown-label {
@apply flex-1 overflow-hidden text-ellipsis whitespace-nowrap;
}
.user-chip {
@apply inline-flex min-h-[38px] min-w-0 max-w-[200px] cursor-pointer items-center gap-2 rounded-[var(--radius-md)] border border-[var(--border)] bg-[rgba(255,255,255,0.03)] py-1 pl-1 pr-2.5 text-left font-extrabold text-[13px] text-[var(--text-primary)] no-underline transition-[transform,border-color,background,color,box-shadow] duration-200 ease-[cubic-bezier(0.4,0,0.2,1)] hover:border-[color-mix(in_srgb,var(--accent)_42%,var(--border))] hover:bg-[var(--bg-card-hover)];
}
.user-chip[aria-expanded="true"] {
@apply border-[color-mix(in_srgb,var(--accent)_55%,var(--border))] bg-[var(--bg-card-hover)];
}
.user-chip-avatar {
@apply h-[28px] w-[28px] flex-none rounded-[var(--radius-sm)] object-cover;
background: color-mix(in srgb, var(--accent) 10%, var(--bg-card));
image-rendering: pixelated;
}
.user-chip-name {
@apply min-w-0 flex-1 overflow-hidden text-ellipsis whitespace-nowrap;
}
.user-chip-caret {
@apply h-3 w-3 flex-none fill-[var(--text-secondary)] transition-transform;
}
.user-chip[aria-expanded="true"] .user-chip-caret {
transform: rotate(180deg);
}
.user-dropdown {
@apply absolute right-0 top-[calc(100%+8px)] z-30 grid w-[min(calc(100vw-28px),280px)] origin-top-right gap-0 rounded-[var(--radius-md)] border border-[var(--border)] bg-[var(--bg-card)] opacity-0 shadow-[0_24px_48px_rgba(0,0,0,0.45)] transition-[opacity,transform] duration-200 ease-[cubic-bezier(0.2,0.8,0.2,1)] pointer-events-none -translate-y-2 scale-[0.96];
backdrop-filter: blur(14px);
will-change: opacity, transform;
}
.user-dropdown.show {
@apply opacity-100 pointer-events-auto translate-y-0 scale-100;
}
.user-dropdown-head {
@apply flex items-center gap-2.5 border-b border-[var(--border)] px-3 py-2.5;
}
.user-dropdown-avatar {
@apply h-[40px] w-[40px] flex-none rounded-[var(--radius-md)] object-cover;
background: color-mix(in srgb, var(--accent) 10%, var(--bg-card));
image-rendering: pixelated;
}
.user-dropdown-head-text {
@apply grid min-w-0 gap-0.5;
}
.user-dropdown-name {
@apply overflow-hidden text-ellipsis whitespace-nowrap text-sm font-extrabold text-[var(--text-primary)];
}
.user-dropdown-sub {
@apply overflow-hidden text-ellipsis whitespace-nowrap font-[family-name:var(--font-mono)] text-[11px] text-[var(--text-muted)];
}
.user-dropdown-rows {
@apply grid;
}
.user-dropdown-row {
@apply flex items-center justify-between gap-3 px-3 py-2.5;
}
.user-dropdown-row + .user-dropdown-row {
@apply border-t border-[var(--border)];
}
.user-dropdown-row-label {
@apply font-[family-name:var(--font-mono)] text-[11px] font-bold text-[var(--text-muted)];
}
.user-dropdown-row-status {
@apply overflow-hidden text-ellipsis whitespace-nowrap text-right text-[13px] font-bold text-[var(--text-primary)];
}
.user-dropdown-row-status.linked {
@apply text-[#6ee7b7];
}
.user-dropdown-row-status.unlinked {
@apply text-[var(--text-muted)];
}
.user-dropdown-logout {
@apply flex min-h-[42px] w-full cursor-pointer items-center justify-center gap-2 rounded-none border-0 border-t border-[var(--border)] bg-transparent px-3 py-2.5 text-center text-[13px] font-extrabold text-[var(--text-secondary)] transition-colors hover:bg-[var(--bg-card-hover)] hover:text-[var(--text-primary)];
}
.user-dropdown-logout svg {
@apply h-4 w-4 fill-current;
}
.sub-status {
@apply flex min-h-[28px] items-center gap-2.5 text-[var(--danger)];
}
.sub-status.active {
@apply text-[var(--success)];
}
.sub-status-dot {
@apply relative inline-block h-[10px] w-[10px] flex-none rounded-full;
background: currentColor;
box-shadow: 0 0 12px currentColor;
}
.sub-status-dot::after {
content: '';
position: absolute;
inset: 0;
border-radius: 9999px;
background: currentColor;
opacity: 0.55;
animation: sub-status-pulse 1.8s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}
.sub-status-text {
@apply font-extrabold text-[15px] leading-tight;
letter-spacing: 0.01em;
}
@keyframes brand-logo-spin {
to {
transform: rotate(360deg);
}
}
.sub-countdown {
@apply grid gap-1;
}
.sub-countdown-row {
@apply flex flex-wrap items-baseline gap-x-2 gap-y-0.5;
}
.sub-countdown-value {
@apply font-extrabold text-[22px] leading-[1.1] text-[var(--text-primary)];
}
.sub-countdown-caption {
@apply font-[family-name:var(--font-mono)] text-[12px] leading-[1.3] text-[var(--text-muted)];
}
.traffic-bar {
@apply relative flex min-h-[24px] w-full items-center justify-center overflow-hidden rounded-full border border-[var(--border)];
background: rgba(255, 255, 255, 0.02);
}
.traffic-bar-fill {
@apply absolute inset-y-0 left-0;
width: 0%;
background: color-mix(in srgb, var(--accent) 28%, transparent);
transition: width 0.3s cubic-bezier(0.4, 0, 0.2, 1), background 0.2s ease;
}
.traffic-bar.over .traffic-bar-fill {
background: color-mix(in srgb, var(--danger) 32%, transparent);
}
.traffic-bar.unlimited .traffic-bar-fill {
width: 100%;
background: color-mix(in srgb, var(--success) 12%, transparent);
}
.traffic-bar.inactive {
border-color: color-mix(in srgb, var(--danger) 60%, transparent);
}
.traffic-bar.inactive .traffic-bar-fill {
width: 100%;
background: color-mix(in srgb, var(--danger) 55%, transparent);
}
.traffic-bar-value {
@apply relative z-[1] min-w-0 px-3 font-[family-name:var(--font-mono)] text-[12px] font-bold text-center whitespace-nowrap;
color: #ffffff;
font-variant-numeric: tabular-nums;
}
.traffic-bar.unlimited .traffic-bar-value {
animation: traffic-infinity-pulse 1.8s ease-in-out infinite;
}
@keyframes traffic-infinity-pulse {
0%, 100% {
opacity: 1;
text-shadow: 0 0 0 color-mix(in srgb, var(--success) 0%, transparent);
}
50% {
opacity: 0.7;
text-shadow: 0 0 10px color-mix(in srgb, var(--success) 70%, transparent);
}
}
.link-cta-head {
@apply grid gap-1;
}
.link-cta-title {
@apply font-extrabold text-[15px] leading-tight text-[var(--text-primary)];
}
.link-cta-caption {
@apply text-[13px] leading-[1.4] text-[var(--text-secondary)];
}
@keyframes sub-status-pulse {
0% {
transform: scale(1);
opacity: 0.55;
}
70% {
transform: scale(2.2);
opacity: 0;
}
100% {
transform: scale(2.2);
opacity: 0;
}
}
.login-head {
@apply flex min-w-0 items-center justify-center gap-3.5;
}
.btn {
@apply inline-flex min-h-[46px] min-w-0 cursor-pointer items-center justify-center gap-2 rounded-[var(--radius-md)] border border-[var(--border)] bg-[rgba(255,255,255,0.03)] px-[13px] py-[11px] text-center font-extrabold text-[var(--text-primary)] no-underline transition-[transform,border-color,background,color,box-shadow] duration-200 ease-[cubic-bezier(0.4,0,0.2,1)] hover:-translate-y-0.5 hover:border-[color-mix(in_srgb,var(--accent)_42%,var(--border))] hover:bg-[var(--bg-card-hover)];
overflow-wrap: anywhere;
}
.btn-primary {
@apply inline-flex min-h-[46px] min-w-0 cursor-pointer items-center justify-center gap-2 rounded-[var(--radius-md)] border border-[color-mix(in_srgb,var(--accent)_80%,var(--border))] bg-[var(--accent)] px-[13px] py-[11px] text-center font-extrabold text-[#04110a] no-underline shadow-[0_10px_26px_color-mix(in_srgb,var(--accent)_18%,transparent)] transition-[transform,border-color,background,color,box-shadow] duration-200 ease-[cubic-bezier(0.4,0,0.2,1)] hover:-translate-y-0.5 hover:bg-[var(--accent-hover)] hover:shadow-[0_14px_34px_color-mix(in_srgb,var(--accent)_24%,transparent)];
overflow-wrap: anywhere;
}
.btn-ghost {
@apply inline-flex min-h-[46px] min-w-0 w-full cursor-pointer items-center justify-center gap-2 rounded-[var(--radius-md)] border border-[color-mix(in_srgb,var(--accent)_35%,var(--border))] bg-[color-mix(in_srgb,var(--accent)_5%,transparent)] px-[13px] py-[11px] text-center font-extrabold text-[var(--accent)] no-underline shadow-none transition-[transform,border-color,background,color,box-shadow] duration-200 ease-[cubic-bezier(0.4,0,0.2,1)] hover:-translate-y-0.5 hover:bg-[color-mix(in_srgb,var(--accent)_10%,transparent)] hover:shadow-[0_10px_24px_color-mix(in_srgb,var(--accent)_10%,transparent)];
overflow-wrap: anywhere;
}
.icon-btn {
@apply inline-flex h-[38px] w-[38px] flex-none cursor-pointer items-center justify-center gap-2 rounded-[var(--radius-md)] border border-[var(--border)] bg-[rgba(255,255,255,0.03)] text-[var(--text-secondary)] transition-[transform,border-color,background,color,box-shadow] duration-200 ease-[cubic-bezier(0.4,0,0.2,1)] hover:-translate-y-0.5 hover:border-[color-mix(in_srgb,var(--accent)_42%,var(--border))] hover:bg-[var(--bg-card-hover)];
}
.input {
@apply min-h-[46px] w-full rounded-[var(--radius-md)] border border-[var(--border)] bg-[rgba(5,8,13,0.42)] px-3 py-[11px] font-bold text-[var(--text-primary)] outline-none transition-[border-color,background,box-shadow] duration-200 ease-[cubic-bezier(0.4,0,0.2,1)] placeholder:text-[var(--text-muted)] focus:border-[color-mix(in_srgb,var(--accent)_55%,var(--border))] focus:bg-[rgba(7,12,19,0.68)] focus:shadow-[0_0_0_3px_color-mix(in_srgb,var(--accent)_12%,transparent),0_12px_28px_rgba(0,0,0,0.2)] aria-[invalid=true]:border-[color-mix(in_srgb,var(--danger)_72%,var(--border))] aria-[invalid=true]:focus:shadow-[0_0_0_3px_color-mix(in_srgb,var(--danger)_14%,transparent)];
}
.input-code {
@apply text-center font-[family-name:var(--font-mono)] tracking-[0];
}
.panel {
@apply relative overflow-hidden rounded-[var(--radius-lg)] border border-[var(--border)] bg-[var(--bg-card)] shadow-[0_20px_40px_rgba(0,0,0,0.18)];
}
.panel-modal {
@apply relative overflow-hidden rounded-[var(--radius-lg)] border border-[var(--border)] bg-[var(--bg-card)] shadow-[0_30px_60px_rgba(0,0,0,0.28)];
}
.step-box {
@apply min-h-[58px] rounded-[var(--radius-md)] border border-[var(--border)] bg-[rgba(255,255,255,0.02)] p-[9px] text-[var(--text-muted)] shadow-[inset_0_1px_0_rgba(255,255,255,0.045)];
}
.modal {
@apply fixed inset-0 z-20 grid place-items-center opacity-0 transition-opacity pointer-events-none;
padding: calc(max(var(--app-safe-top), 14px) + var(--app-safe-top-extra)) 14px max(var(--app-safe-bottom), 14px);
}
.modal.show {
@apply opacity-100 pointer-events-auto;
}
.modal-backdrop,
.tools-modal-backdrop {
@apply absolute inset-0 border-0 p-0;
background: rgba(3, 6, 10, 0.72);
backdrop-filter: blur(7px);
}
.tools-modal-backdrop {
@apply fixed z-25;
}
.modal-card {
@apply relative max-h-[min(86vh,760px)] w-[min(100%,560px)] overflow-auto transition-transform duration-[0.22s] ease-[cubic-bezier(0.2,0.8,0.2,1)];
transform: translateY(18px) scale(0.98);
}
.modal.show .modal-card {
transform: translateY(0) scale(1);
}
.app {
@apply mx-auto flex w-[min(100vw,560px)] max-w-[100vw] flex-col gap-4;
padding: calc(max(var(--app-safe-top), 14px) + var(--app-safe-top-extra)) 14px max(var(--app-safe-bottom), 22px);
}
@media (min-width: 561px) {
.app {
@apply min-h-screen justify-center;
}
}
.telegram-login-widget {
@apply flex min-w-0 items-center;
}
.brand-logo-shell {
@apply relative grid h-[38px] w-[38px] flex-none place-items-center overflow-hidden;
}
.brand-logo-shell--lg {
@apply h-[96px] w-[96px];
}
.brand-logo {
@apply absolute inset-0 h-full w-full object-contain;
}
.brand-logo-spinner {
@apply h-5 w-5 flex-none rounded-full border-2;
border-color: color-mix(in srgb, var(--accent) 18%, var(--border));
border-top-color: var(--accent);
animation: brand-logo-spin 0.85s linear infinite;
}
.brand-logo--lg {
@apply h-[84px] w-[84px];
}
.plans,
.methods,
.metrics,
.referral-link-list,
.bonus-list {
@apply grid min-w-0;
}
.actions,
.otp-slots,
.metric {
@apply grid min-w-0 gap-2;
}
.actions {
grid-template-columns: minmax(0, 1fr) 48px;
}
.metric {
grid-template-columns: minmax(0, 0.88fr) minmax(0, 1.12fr);
}
.metrics {
@apply gap-0 overflow-hidden rounded-[var(--radius-md)] border border-[var(--border)];
background: rgba(255, 255, 255, 0.018);
}
.metric,
.notice,
.otp-slots span {
@apply border border-[var(--border)];
background: rgba(255, 255, 255, 0.02);
}
.metric {
@apply min-h-[46px] items-center gap-2.5 px-3 py-2.5;
}
.metric {
@apply min-h-12;
}
.metric + .metric {
@apply border-t border-[var(--border)];
}
.brand-title,
.login-brand-title {
@apply max-w-[260px] font-[family-name:var(--font-mono)] font-extrabold tracking-[0] text-[var(--accent)];
}
.brand-title {
@apply overflow-hidden text-ellipsis whitespace-nowrap text-[17px] leading-[1.15];
text-shadow: 0 0 20px color-mix(in srgb, var(--accent) 18%, transparent);
}
.login-brand-title {
@apply text-left text-2xl leading-[1.1];
overflow-wrap: anywhere;
}
.login-auth-body {
@apply grid w-full min-h-[140px] gap-2.5 content-start;
}
.login-auth-body--telegram {
@apply content-center;
}
.login-text,
.promo-status {
@apply text-[var(--text-secondary)];
}
.section-label {
@apply min-w-0 font-[family-name:var(--font-mono)] text-[11px] font-bold text-[var(--text-muted)];
overflow-wrap: anywhere;
}
.section-label {
@apply font-extrabold;
}
.mono-pill,
.badge {
@apply inline-flex min-h-7 items-center rounded-[var(--radius-md)] px-2.5 font-[family-name:var(--font-mono)] text-[11px] font-bold whitespace-nowrap;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.06);
}
.mono-pill {
@apply text-[var(--accent)];
border: 1px solid color-mix(in srgb, var(--accent) 45%, var(--border));
background: color-mix(in srgb, var(--accent) 9%, transparent);
}
.badge {
@apply min-w-0 overflow-hidden text-ellipsis text-[#6ee7b7];
border: 1px solid color-mix(in srgb, var(--success) 34%, var(--border));
background: rgba(16, 185, 129, 0.12);
}
.badge.off,
.promo-status.error,
.login-status.error {
@apply text-[#fca5a5];
}
.badge.off {
border-color: color-mix(in srgb, var(--danger) 34%, var(--border));
background: rgba(239, 68, 68, 0.12);
}
.promo-status.success {
@apply text-[#6ee7b7];
}
#legal-links-login {
@apply w-[min(100vw,560px)];
}
.legal-link,
.code-modal-resend {
@apply border-0 bg-transparent p-0 text-[13px] font-bold text-[var(--text-secondary)] underline underline-offset-[3px] transition-colors;
text-decoration-color: color-mix(in srgb, var(--accent) 42%, currentColor);
}
.legal-link {
@apply inline flex-initial;
}
.otp-input {
@apply absolute inset-0 z-2 h-full w-full border-0 bg-transparent text-transparent opacity-[0.01] outline-none;
caret-color: transparent;
}
.otp-slots {
@apply grid-cols-6;
}
.otp-slots span {
@apply grid aspect-square min-w-0 place-items-center rounded-[var(--radius-md)] font-[family-name:var(--font-mono)] text-xl font-[850] leading-none text-[var(--text-primary)] transition-[border-color,background,box-shadow];
background: rgba(5, 8, 13, 0.42);
}
.otp-slots span.filled {
border-color: color-mix(in srgb, var(--accent) 35%, var(--border));
background: color-mix(in srgb, var(--accent) 7%, rgba(255, 255, 255, 0.035));
}
.otp-slots span.active {
border-color: color-mix(in srgb, var(--accent) 72%, var(--border));
box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 12%, transparent);
}
.step-num,
.step-name {
@apply block text-current;
}
.step-num {
@apply font-[family-name:var(--font-mono)] text-[11px] font-extrabold leading-[1.1];
}
.step-name {
@apply mt-1 text-xs font-extrabold leading-[1.15];
}
.step.active,
.auth-tab.active {
@apply border-[var(--accent)] text-[var(--accent)];
background: color-mix(in srgb, var(--accent) 9%, transparent);
}
.step.done {
@apply text-[var(--text-secondary)];
border-color: color-mix(in srgb, var(--accent) 45%, var(--border));
}
.notice {
@apply rounded-[var(--radius-md)] p-[13px] text-sm leading-[1.45] text-[var(--text-secondary)];
}
.login-layout.show {
@apply flex;
}
.telegram-login-widget {
@apply min-h-14 justify-center;
}
.login-text,
.promo-status {
@apply m-0 text-sm leading-[1.45];
}
.promo-status {
@apply mt-0.5 min-h-[22px];
overflow-wrap: anywhere;
}
.login-status {
@apply m-0 min-h-5;
}
.toast {
@apply fixed left-1/2 z-40 w-max max-w-[calc(100vw-28px)] rounded-[var(--radius-md)] px-[13px] py-[11px] text-[13px] font-bold text-[var(--text-primary)] opacity-0 transition-[opacity,transform] duration-300 ease-[cubic-bezier(0.2,0.8,0.2,1)];
bottom: max(var(--app-safe-bottom), 22px);
border: 1px solid color-mix(in srgb, var(--accent) 45%, var(--border));
background: rgba(15, 21, 33, 0.82);
backdrop-filter: blur(14px);
box-shadow: 0 16px 38px rgba(0, 0, 0, 0.45), 0 0 15px color-mix(in srgb, var(--accent) 15%, transparent);
pointer-events: none;
transform: translate(-50%, 14px) scale(0.94);
}
.toast.show {
@apply opacity-100;
transform: translate(-50%, 0) scale(1);
}
}
.hidden {
display: none !important;
}
@supports not (color: color-mix(in srgb, #000 50%, #fff)) {
.mono-pill,
.step.active,
.auth-tab.active {
border-color: var(--accent);
}
.mono-pill,
.step.active,
.auth-tab.active {
background: rgba(0, 254, 122, 0.08);
}
}
@media (hover: hover) and (pointer: fine) {
.legal-link:hover:not(:disabled),
.code-modal-resend:hover:not(:disabled) {
@apply text-[var(--accent)];
}
}
@media (max-width: 460px) {
.app {
@apply px-2.5;
}
.user-chip {
@apply max-w-[150px];
}
.user-chip-name {
@apply hidden;
}
.brand-title {
@apply max-w-[164px];
}
.metric {
@apply grid-cols-1;
}
.badge,
.mono-pill {
@apply max-w-full;
}
.otp-slots {
@apply gap-1.5;
}
.otp-slots span {
@apply text-lg;
}
.brand-logo--lg {
@apply h-[68px] w-[68px];
}
.brand-logo-shell--lg {
@apply h-[96px] w-[96px];
}
.login-auth-body {
@apply min-h-[128px];
}
.login-brand-title {
@apply max-w-[200px] text-[21px];
}
}
@media (max-width: 380px) {
.brand-title {
@apply max-w-[150px];
}
}
@media (max-width: 560px) {
.step-box {
@apply min-h-[54px] px-[7px] py-2;
}
.modal {
@apply place-items-center p-0;
top: 0;
bottom: 0;
padding: calc(max(var(--app-safe-top), 14px) + var(--app-safe-top-extra)) 14px max(var(--app-safe-bottom), 14px);
}
.modal--page {
@apply place-items-stretch p-0;
padding: 0 !important;
}
.modal--page .modal-backdrop {
@apply bg-[var(--bg-primary)] opacity-100;
backdrop-filter: none;
}
.modal-card {
@apply h-auto max-h-[min(92vh,760px)] w-full rounded-[var(--radius-lg)];
transform: translateY(12px) scale(0.98);
}
.modal--page .modal-card {
@apply h-full w-full max-h-none max-w-none rounded-none border-0 shadow-none;
transform: none !important;
}
.modal.show .modal-card {
transform: translateY(0) scale(1);
}
}
+114 -8
View File
@@ -57,7 +57,6 @@
"pay_with_stars_button": "🌟 Telegram Stars",
"connect_button": "🔗 Connect",
"cancel_button": "❌ Cancel",
"devices_button": "📱 My Devices ({current_devices}/{max_devices})",
"my_devices_details": "📱 <b>My Devices ({current_devices}/{max_devices})</b>\n\n{devices}\n\nYou can disconnect a device by selecting it from the list below.\n<blockquote><i>Note: If you disconnect a device, it will be automatically connected again when you use it next. Before deleting, make sure you have deleted the subscription from the application.</i></blockquote>",
"no_devices_details_found_message": "📱 <b>My Devices</b>\n\nYou don't have any devices yet.\nYou have {max_devices} devices available. You can connect them by clicking the \"🔗 Connect\" button in the subscription menu.",
@@ -67,7 +66,6 @@
"no_devices_found": "Error loading device list.",
"devices_unlimited_label": "Unlimited",
"my_devices_feature_disabled": "The My Devices section is currently unavailable.",
"payment_description_subscription": "Subscription payment for {months} mo.",
"payment_description_traffic": "Traffic package {traffic_gb} GB",
"payment_link_message": "To pay for {months} mo. subscription, click the button below:",
@@ -522,9 +520,7 @@
"admin_ads_deleted_success": "Campaign deleted.",
"admin_ads_not_found": "Campaign not found.",
"free_kassa_order_full": "Order #{order_id} from {date}\n\n",
"email_footer_auto": "Sent automatically by {brand}. Please don't reply.",
"email_login_code_subject": "{code} — your sign-in code",
"email_login_code_preheader": "Your one-time code expires in {minutes} min.",
"email_login_code_heading": "Confirm your sign-in",
@@ -537,7 +533,6 @@
"email_login_code_magic_or": "or",
"email_login_code_text": "Your verification code: {code}\n\nThe code is valid for {minutes} min.\nIf you didn't request this code, ignore this message.",
"email_login_code_text_magic": "Or sign in with this single-use link: {url}",
"email_account_merged_subject": "Accounts merged",
"email_account_merged_preheader": "Your accounts were combined into one profile.",
"email_account_merged_heading": "Accounts merged",
@@ -547,7 +542,6 @@
"email_account_merged_row_end_date": "New end date",
"email_account_merged_note": "Your subscription link is unchanged. The duplicate panel user was removed automatically.",
"email_account_merged_text": "We merged your accounts into one profile.\n\nKept account: {primary}\nRemoved account: {removed}\nPaid periods were combined. New subscription end date: {end_date}.\nYour subscription link stayed the same, and the later account was removed from Remnawave automatically.",
"email_payment_success_subject": "Payment received",
"email_payment_success_preheader": "Thanks — your subscription has been activated.",
"email_payment_success_heading": "Payment received",
@@ -565,7 +559,6 @@
"email_payment_success_text_subscription": "Payment received: {amount}.\nSubscription extended by {months} month(s).\nActive until: {end_date}.",
"email_payment_success_text_traffic": "Payment received: {amount}.\nTraffic added: {traffic_gb} GB.\nActive until: {end_date}.",
"email_payment_success_text_dashboard": "Dashboard: {url}",
"email_subscription_expiring_subject_today": "Your subscription ends today",
"email_subscription_expiring_subject_tomorrow": "1 day left on your subscription",
"email_subscription_expiring_subject_days": "{days} days left on your subscription",
@@ -583,5 +576,118 @@
"email_subscription_expiring_cta": "Renew subscription",
"email_subscription_expiring_note": "If you've already renewed or use auto-renewal, you can ignore this email.",
"email_subscription_expiring_text": "{heading}.\nActive until: {end_date}.",
"email_subscription_expiring_text_renew": "Renew: {url}"
"email_subscription_expiring_text_renew": "Renew: {url}",
"wa_loading": "Loading...",
"wa_back": "Back",
"wa_close": "Close",
"wa_auth_checking_login": "Checking sign-in...",
"wa_auth_login_confirm_failed": "Could not confirm sign-in",
"wa_auth_checking_telegram": "Checking Telegram...",
"wa_auth_access_denied": "Access denied",
"wa_auth_telegram_not_confirmed": "Telegram sign-in not confirmed",
"wa_auth_telegram_unavailable": "Telegram sign-in is currently unavailable",
"wa_auth_telegram_not_configured": "Telegram sign-in is not configured",
"wa_auth_telegram_cancelled": "Telegram sign-in was cancelled",
"wa_auth_invalid_email": "Enter a valid email",
"wa_auth_sending_code": "Sending code...",
"wa_auth_send_code_failed": "Failed to send code",
"wa_auth_enter_code_6digits": "Enter the 6-digit code from the email",
"wa_auth_checking_code": "Checking code...",
"wa_auth_invalid_code": "Invalid code",
"wa_auth_code_expired": "Code expired",
"wa_auth_resend_wait": "You can resend in {seconds} sec.",
"wa_email_verification_title": "Email confirmation",
"wa_email_sent_to": "We sent a code to {email}",
"wa_email_code_aria": "Verification code",
"wa_email_placeholder": "Email",
"wa_confirm": "Confirm",
"wa_resend_code": "Resend code",
"wa_send_code_email": "Send code to email",
"wa_or": "or",
"wa_login_telegram_button": "Sign in with Telegram",
"wa_auth_legal_intro": "By creating an account, you agree to the",
"wa_auth_legal_privacy": "privacy policy",
"wa_auth_legal_and": "and",
"wa_auth_legal_agreement": "user agreement",
"wa_home_subscription_active": "Subscription active",
"wa_sub_term_forever": "Forever",
"wa_sub_term_value_unit": "{value} {unit}",
"wa_sub_term_day_one": "day",
"wa_sub_term_day_few": "days",
"wa_sub_term_day_many": "days",
"wa_sub_term_month_one": "month",
"wa_sub_term_month_few": "months",
"wa_sub_term_month_many": "months",
"wa_sub_term_year_one": "year",
"wa_sub_term_year_few": "years",
"wa_sub_term_year_many": "years",
"wa_home_subscription_inactive": "Subscription inactive",
"wa_until_date": "until {date}",
"wa_home_traffic_used": "Traffic used",
"wa_renew": "Renew",
"wa_pay_subscription": "Pay subscription",
"wa_subscription_title": "Subscription",
"wa_subscription_choose_period": "Choose subscription period",
"wa_per_month_short": "/mo",
"wa_payment_methods_not_configured": "Payment methods are not configured yet",
"wa_pay": "Pay",
"wa_referral_link_title": "Your referral link",
"wa_link_unavailable": "Link is not available yet",
"wa_link_copied": "Link copied",
"wa_copy": "Copy",
"wa_referral_bonus_overview_title": "Bonus system",
"wa_referral_bonus_once_note": "Bonus is granted only once per invited user after their first payment.",
"wa_referral_bonus_registration_title": "For registration via referral link",
"wa_referral_bonus_paid_intro": "If a friend pays for a subscription:",
"wa_referral_bonus_you_days": "You: +{days} days",
"wa_referral_bonus_friend_days": "Friend: +{days} days",
"wa_referral_bonus_not_configured": "Referral bonuses are not configured",
"wa_promo_enter": "Enter promo code",
"wa_promo_activated_until": "Promo code activated. Subscription until {date}",
"wa_promo_activated": "Promo code activated",
"wa_promo_activation_failed": "Failed to activate promo code",
"wa_activate": "Activate",
"wa_settings_avatar_alt": "User avatar",
"wa_settings_language": "Language",
"wa_settings_link_telegram": "Telegram linked",
"wa_settings_not_linked": "Not linked",
"wa_settings_link_email": "Email linked",
"wa_settings_email_not_linked": "Not linked",
"wa_settings_linked": "Linked",
"wa_settings_language_update_failed": "Failed to update language",
"wa_settings_user_agreement": "User agreement",
"wa_settings_privacy_policy": "Privacy policy",
"wa_logout": "Log out",
"wa_end_session": "End session",
"wa_navigation": "Navigation",
"wa_nav_home": "Home",
"wa_nav_settings": "Settings",
"wa_payment_created": "Payment created",
"wa_payment_create_failed": "Failed to create payment",
"wa_connect_link_unavailable": "Connection link is not available yet",
"wa_copied": "Copied",
"wa_unavailable": "Not available yet",
"wa_method_other_title": "Other methods",
"wa_unlimited_traffic": "Unlimited traffic",
"wa_traffic_of": "{used} of {limit}",
"wa_language_default": "English",
"wa_telegram_not_linked": "Telegram not linked",
"wa_tg_id_not_linked": "TG ID not linked",
"wa_nav_bonuses": "Bonuses",
"wa_method_platega_sbp_card": "SBP/Card",
"wa_method_platega_crypto": "Crypto",
"wa_settings_link_telegram_action": "Link Telegram",
"wa_settings_telegram_linked_title": "Telegram linked",
"wa_settings_link_email_action": "Link email",
"wa_settings_email_linked_title": "Email linked",
"wa_activate_promo_title": "Activate promo code",
"wa_link_email_modal_title": "Link email",
"wa_traffic_reset_none": "No reset",
"wa_traffic_reset_monthly": "Monthly reset",
"wa_traffic_reset_weekly": "Weekly reset",
"wa_traffic_reset_daily": "Daily reset",
"wa_traffic_reset_yearly": "Yearly reset",
"wa_traffic_reset_policy": "Traffic reset policy",
"wa_plan_one_year": "1 year",
"wa_link_email_modal_desc": "Enter your email and get a verification code"
}
+114 -8
View File
@@ -56,7 +56,6 @@
"pay_with_cryptopay_button": "💎 CryptoBot",
"pay_with_stars_button": "🌟 Звезды Telegram",
"connect_button": "🔗 Подключиться",
"devices_button": "📱 Мои устройства ({current_devices}/{max_devices})",
"my_devices_details": "📱 <b>Список ваших устройств ({current_devices}/{max_devices})</b>\n\n{devices}\n\nВы можете отключить устройство, выбрав его в списке ниже.\n<blockquote><i>Примечание: Если вы отключили устройство, оно будет автоматически подключено заново при следующем использовании. Перед удалением убедитесь, что вы удалили подписку из приложения.</i></blockquote>",
"no_devices_details_found_message": "📱 <b>Список ваших устройств</b>\n\nУ вас пока нет устройств.\nВам доступно {max_devices} устройств. Подключить их можно через кнопку \"🔗 Подключиться\" в меню подписки.",
@@ -66,7 +65,6 @@
"no_devices_found": "Ошибка загрузки списка устройств.",
"devices_unlimited_label": "Без ограничений",
"my_devices_feature_disabled": "Раздел \"Мои устройства\" сейчас недоступен.",
"cancel_button": "❌ Отмена",
"payment_description_subscription": "Оплата подписки на {months} мес.",
"payment_description_traffic": "Пакет трафика {traffic_gb} ГБ",
@@ -522,9 +520,7 @@
"admin_ads_deleted_success": "Кампания удалена.",
"admin_ads_not_found": "Кампания не найдена.",
"free_kassa_order_full": "Заказ №{order_id} от {date}\n\n",
"email_footer_auto": "Это автоматическое письмо от {brand}. Отвечать на него не нужно.",
"email_login_code_subject": "{code} — код для входа",
"email_login_code_preheader": "Одноразовый код действует {minutes} мин.",
"email_login_code_heading": "Подтвердите вход",
@@ -537,7 +533,6 @@
"email_login_code_magic_or": "или",
"email_login_code_text": "Ваш код подтверждения: {code}\n\nКод действует {minutes} мин.\nЕсли вы не запрашивали код, проигнорируйте это письмо.",
"email_login_code_text_magic": "Или войдите по ссылке (одноразовая): {url}",
"email_account_merged_subject": "Аккаунты объединены",
"email_account_merged_preheader": "Мы объединили ваши аккаунты в один профиль.",
"email_account_merged_heading": "Аккаунты объединены",
@@ -547,7 +542,6 @@
"email_account_merged_row_end_date": "Новая дата окончания",
"email_account_merged_note": "Ссылка на подписку осталась прежней. Дублирующий пользователь панели был удалён автоматически.",
"email_account_merged_text": "Мы объединили ваши аккаунты в один профиль.\n\nОставлен аккаунт: {primary}\nУдалён аккаунт: {removed}\nОплаченные периоды сложились. Новая дата окончания подписки: {end_date}.\nСсылка на подписку осталась прежней, а более поздний аккаунт был удалён из Remnawave автоматически.",
"email_payment_success_subject": "Платёж получен",
"email_payment_success_preheader": "Спасибо — подписка активирована.",
"email_payment_success_heading": "Платёж получен",
@@ -565,7 +559,6 @@
"email_payment_success_text_subscription": "Платёж получен: {amount}.\nПодписка продлена на {months} мес.\nДействует до: {end_date}.",
"email_payment_success_text_traffic": "Платёж получен: {amount}.\nЗачислено трафика: {traffic_gb} ГБ.\nДействует до: {end_date}.",
"email_payment_success_text_dashboard": "Кабинет: {url}",
"email_subscription_expiring_subject_today": "Подписка заканчивается сегодня",
"email_subscription_expiring_subject_tomorrow": "Подписка заканчивается завтра",
"email_subscription_expiring_subject_days": "До конца подписки осталось {days} дн.",
@@ -583,5 +576,118 @@
"email_subscription_expiring_cta": "Продлить подписку",
"email_subscription_expiring_note": "Если вы уже продлили или включили автопродление — просто проигнорируйте это письмо.",
"email_subscription_expiring_text": "{heading}.\nДействует до: {end_date}.",
"email_subscription_expiring_text_renew": "Продлить: {url}"
"email_subscription_expiring_text_renew": "Продлить: {url}",
"wa_loading": "Загрузка...",
"wa_back": "Назад",
"wa_close": "Закрыть",
"wa_auth_checking_login": "Проверяем вход...",
"wa_auth_login_confirm_failed": "Не удалось подтвердить вход",
"wa_auth_checking_telegram": "Проверяем Telegram...",
"wa_auth_access_denied": "Доступ запрещен",
"wa_auth_telegram_not_confirmed": "Telegram-вход не подтвержден",
"wa_auth_telegram_unavailable": "Telegram-вход сейчас недоступен",
"wa_auth_telegram_not_configured": "Telegram-вход не настроен",
"wa_auth_telegram_cancelled": "Вход через Telegram отменён",
"wa_auth_invalid_email": "Введите корректный email",
"wa_auth_sending_code": "Отправляем код...",
"wa_auth_send_code_failed": "Не удалось отправить код",
"wa_auth_enter_code_6digits": "Введите 6 цифр из письма",
"wa_auth_checking_code": "Проверяем код...",
"wa_auth_invalid_code": "Неверный код",
"wa_auth_code_expired": "Код устарел",
"wa_auth_resend_wait": "Повторная отправка через {seconds} сек.",
"wa_email_verification_title": "Подтверждение по email",
"wa_email_sent_to": "Мы отправили код на {email}",
"wa_email_code_aria": "Код подтверждения",
"wa_email_placeholder": "Email",
"wa_confirm": "Подтвердить",
"wa_resend_code": "Отправить код повторно",
"wa_send_code_email": "Отправить код на почту",
"wa_or": "или",
"wa_login_telegram_button": "Войти через телеграм",
"wa_auth_legal_intro": "Создавая аккаунт, вы соглашаетесь с",
"wa_auth_legal_privacy": "политикой конфиденциальности",
"wa_auth_legal_and": "и",
"wa_auth_legal_agreement": "пользовательским соглашением",
"wa_home_subscription_active": "Подписка активна",
"wa_sub_term_forever": "Навсегда",
"wa_sub_term_value_unit": "{value} {unit}",
"wa_sub_term_day_one": "день",
"wa_sub_term_day_few": "дня",
"wa_sub_term_day_many": "дней",
"wa_sub_term_month_one": "месяц",
"wa_sub_term_month_few": "месяца",
"wa_sub_term_month_many": "месяцев",
"wa_sub_term_year_one": "год",
"wa_sub_term_year_few": "года",
"wa_sub_term_year_many": "лет",
"wa_home_subscription_inactive": "Подписка не активна",
"wa_until_date": "до {date}",
"wa_home_traffic_used": "Использовано трафика",
"wa_renew": "Продлить",
"wa_pay_subscription": "Оплатить подписку",
"wa_subscription_title": "Подписка",
"wa_subscription_choose_period": "Выберите срок подписки",
"wa_per_month_short": "/мес",
"wa_payment_methods_not_configured": "Способы оплаты пока не настроены",
"wa_pay": "Оплатить",
"wa_referral_link_title": "Ваша реферальная ссылка",
"wa_link_unavailable": "Ссылка пока недоступна",
"wa_link_copied": "Ссылка скопирована",
"wa_copy": "Копировать",
"wa_referral_bonus_overview_title": "Система бонусов",
"wa_referral_bonus_once_note": "Бонус за приглашённого начисляется только один раз, после его первой оплаты.",
"wa_referral_bonus_registration_title": "За регистрацию по реферальной ссылке",
"wa_referral_bonus_paid_intro": "Если друг оплатит подписку:",
"wa_referral_bonus_you_days": "Вам: +{days} дней",
"wa_referral_bonus_friend_days": "Другу: +{days} дней",
"wa_referral_bonus_not_configured": "Реферальные бонусы не настроены",
"wa_promo_enter": "Введите промокод",
"wa_promo_activated_until": "Промокод активирован. Подписка до {date}",
"wa_promo_activated": "Промокод активирован",
"wa_promo_activation_failed": "Не удалось активировать промокод",
"wa_activate": "Активировать",
"wa_settings_avatar_alt": "Аватар пользователя",
"wa_settings_language": "Выбор языка",
"wa_settings_link_telegram": "Привязка Telegram",
"wa_settings_not_linked": "Не привязан",
"wa_settings_link_email": "Привязка почты",
"wa_settings_email_not_linked": "Не привязана",
"wa_settings_linked": "Привязано",
"wa_settings_language_update_failed": "Не удалось обновить язык",
"wa_settings_user_agreement": "Пользовательское соглашение",
"wa_settings_privacy_policy": "Политика конфиденциальности",
"wa_logout": "Выйти",
"wa_end_session": "Завершить сессию",
"wa_navigation": "Навигация",
"wa_nav_home": "Главная",
"wa_nav_settings": "Настройки",
"wa_payment_created": "Платеж создан",
"wa_payment_create_failed": "Не удалось создать платеж",
"wa_connect_link_unavailable": "Ссылка для подключения пока недоступна",
"wa_copied": "Скопировано",
"wa_unavailable": "Пока недоступно",
"wa_method_other_title": "Другие способы",
"wa_unlimited_traffic": "Безлимитный трафик",
"wa_traffic_of": "{used} из {limit}",
"wa_language_default": "Русский",
"wa_telegram_not_linked": "Telegram не привязан",
"wa_tg_id_not_linked": "TG ID не привязан",
"wa_nav_bonuses": "Бонусы",
"wa_method_platega_sbp_card": "СБП/Карта",
"wa_method_platega_crypto": "Крипта",
"wa_settings_link_telegram_action": "Привязать телеграм",
"wa_settings_telegram_linked_title": "Телеграм привязан",
"wa_settings_link_email_action": "Привязать почту",
"wa_settings_email_linked_title": "Почта привязана",
"wa_activate_promo_title": "Активировать промокод",
"wa_link_email_modal_title": "Привязать почту",
"wa_traffic_reset_none": "Без сброса",
"wa_traffic_reset_monthly": "Сброс ежемесячно",
"wa_traffic_reset_weekly": "Сброс еженедельно",
"wa_traffic_reset_daily": "Сброс ежедневно",
"wa_traffic_reset_yearly": "Сброс ежегодно",
"wa_traffic_reset_policy": "Стратегия сброса трафика",
"wa_plan_one_year": "1 год",
"wa_link_email_modal_desc": "Введите email и получите код подтверждения"
}