Merge branch 'dev' into feature/new-tariffs
# Conflicts: # .gitignore # bot/app/web/subscription_webapp.py # tests/test_settings.py
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,217 @@
|
||||
<script>
|
||||
import {
|
||||
ArrowRight,
|
||||
CheckCircle2,
|
||||
Circle,
|
||||
Copy,
|
||||
Crown,
|
||||
Database,
|
||||
Download,
|
||||
Gift,
|
||||
Globe2,
|
||||
LockKeyhole,
|
||||
Mail,
|
||||
RefreshCw,
|
||||
Repeat2,
|
||||
Send,
|
||||
Ticket,
|
||||
UserRound,
|
||||
Zap,
|
||||
} from "lucide-svelte";
|
||||
|
||||
import Button from "./lib/components/ui/button.svelte";
|
||||
import Card from "./lib/components/ui/card.svelte";
|
||||
import BackTitle from "./preview/BackTitle.svelte";
|
||||
import PhoneFrame from "./preview/PhoneFrame.svelte";
|
||||
import PreviewMethods from "./preview/PreviewMethods.svelte";
|
||||
import PreviewNav from "./preview/PreviewNav.svelte";
|
||||
|
||||
export let config = {};
|
||||
export let mockData = {};
|
||||
|
||||
const title = config.title || "/minishop";
|
||||
const logoEmoji = config.logoEmoji || "🫥";
|
||||
const plans = mockData.plans || [];
|
||||
const sub = mockData.subscription || {};
|
||||
const methods = mockData.payment_methods || [];
|
||||
const user = mockData.user || {};
|
||||
const tariffs = [
|
||||
["subscription", "Подписка", "Безлимитный трафик", "Идеально для постоянного использования", Zap],
|
||||
["traffic", "Трафик", "Пакеты гигабайт", "Платите только за нужный объем", Database],
|
||||
["premium", "Премиум", "Максимальная скорость", "Приоритетные серверы и поддержка", Crown],
|
||||
];
|
||||
const traffic = [
|
||||
[20, 290],
|
||||
[50, 590],
|
||||
[100, 990],
|
||||
[300, 2190],
|
||||
];
|
||||
const settingsRows = [
|
||||
[Globe2, "Язык интерфейса", "Русский"],
|
||||
[Send, "Привязка Telegram", user.telegram_linked ? `@${user.username || "username"}` : "Не привязан"],
|
||||
[Mail, "Привязка почты", user.email || "Не привязана"],
|
||||
[UserRound, "Выйти", "Завершить сессию"],
|
||||
];
|
||||
const previewTelegramName = user.first_name || (user.username ? `@${user.username}` : "Telegram не привязан");
|
||||
const previewEmail = user.email || "Почта не привязана";
|
||||
const previewTelegramId = user.telegram_id ? `TG ID ${user.telegram_id}` : "TG ID не привязан";
|
||||
const previewAvatar = user.telegram_photo_url || "";
|
||||
|
||||
function money(value) {
|
||||
return `${value} ₽`;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="preview-board" style={`--accent: ${config.primaryColor || "#00fe7a"};`}>
|
||||
<PhoneFrame number="1" label="Главный экран">
|
||||
<main class="home-layout">
|
||||
<div class="login-brand home-brand">
|
||||
<div class="brand-mark brand-mark-xl"><span>{logoEmoji}</span></div>
|
||||
<h1>{title}</h1>
|
||||
</div>
|
||||
<div class="home-bottom">
|
||||
<Card class="status-card">
|
||||
<div class="sub-status">
|
||||
<CheckCircle2 size={23} />
|
||||
<div><h2>Подписка активна</h2><p>до {sub.end_date_text}</p></div>
|
||||
</div>
|
||||
</Card>
|
||||
<Card>
|
||||
<div class="traffic-top"><span>Использовано трафика</span><strong>{sub.traffic_used} из {sub.traffic_limit}</strong></div>
|
||||
<div class="progress"><span style="width: 18%"></span></div>
|
||||
<div class="traffic-percent">18%</div>
|
||||
</Card>
|
||||
<div class="action-stack">
|
||||
<Button class="wide"><Download size={17} />Установить и настроить</Button>
|
||||
<Button variant="secondary" class="wide"><RefreshCw size={17} />Продлить</Button>
|
||||
<Button variant="secondary" class="wide"><Repeat2 size={17} />Сменить тариф</Button>
|
||||
</div>
|
||||
</div>
|
||||
<PreviewNav active="home" />
|
||||
</main>
|
||||
</PhoneFrame>
|
||||
|
||||
<PhoneFrame number="2" label="Выбор тарифа">
|
||||
<div class="preview-header"><div class="brand-row"><div class="brand-mark"><span>{logoEmoji}</span></div><strong>{title}</strong></div></div>
|
||||
<div class="tariff-list">
|
||||
{#each tariffs as tariff, index}
|
||||
<div class:active={index === 0} class="select-card">
|
||||
<span class="select-icon"><svelte:component this={tariff[4]} size={24} /></span>
|
||||
<span><strong>{tariff[1]}</strong><small>{tariff[2]}</small><em>{tariff[3]}</em></span>
|
||||
{#if index === 0}<CheckCircle2 size={21} />{:else}<Circle size={21} />{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
<Button class="wide bottom-action">Далее <ArrowRight size={17} /></Button>
|
||||
</PhoneFrame>
|
||||
|
||||
<PhoneFrame number="3" label="Оплата тарифа — подписка" wide>
|
||||
<BackTitle title="Подписка" subtitle="Выберите срок подписки" />
|
||||
<div class="period-grid">
|
||||
{#each plans as plan, index}
|
||||
<div class:active={index === 1} class="period-card">
|
||||
<strong>{plan.title}</strong><span>{money(plan.price)}</span><small>{money(Math.round(plan.price / plan.months))}/мес</small>
|
||||
{#if index === 1}<CheckCircle2 size={18} />{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
<Card class="total-card"><span>Итого<br /><small>К оплате</small></span><strong>790 ₽</strong></Card>
|
||||
<PreviewMethods {methods} />
|
||||
<Button class="wide bottom-action">Оплатить 790 ₽ <LockKeyhole size={16} /></Button>
|
||||
</PhoneFrame>
|
||||
|
||||
<PhoneFrame number="4" label="Оплата тарифа — трафик" wide>
|
||||
<BackTitle title="Трафик" subtitle="Выберите пакет трафика" />
|
||||
<div class="period-grid">
|
||||
{#each traffic as pack, index}
|
||||
<div class:active={index === 2} class="period-card">
|
||||
<strong>{pack[0]} ГБ</strong><span>{money(pack[1])}</span><small>{money(Math.round(pack[1] / pack[0]))}/ГБ</small>
|
||||
{#if index === 2}<CheckCircle2 size={18} />{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
<Card class="total-card"><span>Итого<br /><small>К оплате</small></span><strong>990 ₽</strong></Card>
|
||||
<PreviewMethods {methods} />
|
||||
<Button class="wide bottom-action">Оплатить 990 ₽ <LockKeyhole size={16} /></Button>
|
||||
</PhoneFrame>
|
||||
|
||||
<PhoneFrame number="5" label="Смена тарифа">
|
||||
<BackTitle title="Смена тарифа" subtitle="Остаток 12 дней будет пересчитан" />
|
||||
<div class="tariff-list compact">
|
||||
<div class="select-card"><span><strong>Подписка</strong><small>Безлимитный трафик</small></span><em>Доплата 190 ₽</em><Circle size={20} /></div>
|
||||
<div class="select-card active"><span><strong>Трафик</strong><small>Пакеты гигабайт</small></span><em>Доплата не требуется</em><CheckCircle2 size={20} /></div>
|
||||
<div class="select-card"><span><strong>Премиум</strong><small>Максимальная скорость</small></span><em>Доплата 390 ₽</em><Circle size={20} /></div>
|
||||
</div>
|
||||
<Button class="wide bottom-action">Далее <ArrowRight size={17} /></Button>
|
||||
<div class="preview-modal">
|
||||
<Repeat2 size={30} />
|
||||
<strong>Сменить тариф без доплаты?</strong>
|
||||
<p>Остаток 12 дней будет пересчитан по новому тарифу.</p>
|
||||
<Button>Да, сменить</Button>
|
||||
<Button variant="secondary">Отмена</Button>
|
||||
</div>
|
||||
</PhoneFrame>
|
||||
|
||||
<PhoneFrame number="6" label="Пригласить друга">
|
||||
<div class="preview-header"><div class="brand-row"><div class="brand-mark"><span>{logoEmoji}</span></div><strong>{title}</strong></div></div>
|
||||
<Card>
|
||||
<div class="card-label">Ваша реферальная ссылка</div>
|
||||
<div class="copy-row"><code>https://minishop.app/ref/ABCD1234</code><Button>Копировать <Copy size={16} /></Button></div>
|
||||
</Card>
|
||||
<Card class="bonus-card">
|
||||
<Gift size={42} /><div><span>Ваш бонус</span><strong>+7 дней за каждого друга</strong><p>Друг получит +3 дня к подписке.</p></div>
|
||||
</Card>
|
||||
<Button variant="outline" class="wide"><Ticket size={18} />Активировать промокод</Button>
|
||||
</PhoneFrame>
|
||||
|
||||
<PhoneFrame number="7" label="Настройки">
|
||||
<div class="preview-header"><div class="brand-row"><div class="brand-mark"><span>{logoEmoji}</span></div><strong>{title}</strong></div></div>
|
||||
<Card class="settings-profile">
|
||||
<div class="settings-avatar">
|
||||
{#if previewAvatar}
|
||||
<img src={previewAvatar} alt="Аватар пользователя" />
|
||||
{:else}
|
||||
<UserRound size={27} />
|
||||
{/if}
|
||||
</div>
|
||||
<div class="settings-profile-meta">
|
||||
<strong>{previewTelegramName}</strong>
|
||||
<small>{previewEmail}</small>
|
||||
<small>{previewTelegramId}</small>
|
||||
</div>
|
||||
</Card> <div class="settings-list">
|
||||
{#each settingsRows as row}
|
||||
<div class="settings-row">
|
||||
<svelte:component this={row[0]} size={20} />
|
||||
<span><strong>{row[1]}</strong><small>{row[2]}</small></span>
|
||||
<ArrowRight size={16} />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</PhoneFrame>
|
||||
|
||||
<PhoneFrame number="8" label="Логин" wide>
|
||||
<div class="login-brand small">
|
||||
<div class="brand-mark brand-mark-xl"><span>{logoEmoji}</span></div>
|
||||
<h1>{title}</h1><p>Войдите в свой аккаунт</p>
|
||||
</div>
|
||||
<Card class="auth-card">
|
||||
<div class="field-label">Вход по email</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 telegram-login-button">
|
||||
<span class="telegram-login-text"><Send size={17} />Войти через телеграм</span>
|
||||
</Button>
|
||||
</Card>
|
||||
</PhoneFrame>
|
||||
|
||||
<PhoneFrame number="9" label="Подтверждение по коду" wide>
|
||||
<BackTitle title="Подтверждение по email" subtitle="Мы отправили код на user@example.com" />
|
||||
<div class="otp-slots static">{#each [1, 2, 3, 4, 5, 6] as digit}<span>{digit}</span>{/each}</div>
|
||||
<Button class="wide bottom-action">Подтвердить</Button>
|
||||
<button class="link-button"><RefreshCw size={15} />Отправить код повторно (00:45)</button>
|
||||
</PhoneFrame>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
<script>
|
||||
import { cn } from "../../utils.js";
|
||||
|
||||
export let type = "button";
|
||||
export let variant = "default";
|
||||
export let size = "default";
|
||||
export let disabled = false;
|
||||
export let href = "";
|
||||
export let onclick = undefined;
|
||||
let className = "";
|
||||
export { className as class };
|
||||
|
||||
const variants = {
|
||||
default: "btn btn-primary",
|
||||
secondary: "btn btn-secondary",
|
||||
outline: "btn btn-outline",
|
||||
ghost: "btn btn-ghost",
|
||||
telegram: "btn btn-telegram",
|
||||
icon: "btn btn-icon",
|
||||
};
|
||||
|
||||
const sizes = {
|
||||
default: "",
|
||||
sm: "btn-sm",
|
||||
lg: "btn-lg",
|
||||
icon: "btn-square",
|
||||
};
|
||||
</script>
|
||||
|
||||
{#if href}
|
||||
<a class={cn(variants[variant], sizes[size], className)} {href} on:click={onclick} {...$$restProps}>
|
||||
<slot />
|
||||
</a>
|
||||
{:else}
|
||||
<button class={cn(variants[variant], sizes[size], className)} {type} {disabled} on:click={onclick} {...$$restProps}>
|
||||
<slot />
|
||||
</button>
|
||||
{/if}
|
||||
@@ -0,0 +1,12 @@
|
||||
<script>
|
||||
import { cn } from "../../utils.js";
|
||||
|
||||
export let active = false;
|
||||
export let compact = false;
|
||||
let className = "";
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<section class={cn("card", active && "card-active", compact && "card-compact", className)}>
|
||||
<slot />
|
||||
</section>
|
||||
@@ -0,0 +1,31 @@
|
||||
<script>
|
||||
import { X } from "lucide-svelte";
|
||||
import { cn } from "../../utils.js";
|
||||
import Button from "./button.svelte";
|
||||
|
||||
export let open = false;
|
||||
export let title = "";
|
||||
export let description = "";
|
||||
export let closeLabel = "Close";
|
||||
export let onclose = () => {};
|
||||
let className = "";
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
{#if open}
|
||||
<div class="dialog" role="dialog" aria-modal="true" aria-label={title}>
|
||||
<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={closeLabel}>
|
||||
<X size={18} />
|
||||
</Button>
|
||||
</div>
|
||||
<slot />
|
||||
</section>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -0,0 +1,29 @@
|
||||
<script>
|
||||
import { cn } from "../../utils.js";
|
||||
|
||||
export let value = "";
|
||||
export let type = "text";
|
||||
export let placeholder = "";
|
||||
export let inputmode = undefined;
|
||||
export let maxlength = undefined;
|
||||
export let autocomplete = undefined;
|
||||
export let disabled = false;
|
||||
let className = "";
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<input
|
||||
bind:value
|
||||
class={cn("input", className)}
|
||||
on:keydown
|
||||
on:input
|
||||
on:focus
|
||||
on:blur
|
||||
{type}
|
||||
{placeholder}
|
||||
{inputmode}
|
||||
{maxlength}
|
||||
{autocomplete}
|
||||
{disabled}
|
||||
{...$$restProps}
|
||||
/>
|
||||
@@ -0,0 +1,6 @@
|
||||
import { clsx } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export function cn(...inputs) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { mount } from "svelte";
|
||||
|
||||
import App from "./App.svelte";
|
||||
import "./styles.css";
|
||||
|
||||
const target = document.getElementById("app");
|
||||
|
||||
if (target) {
|
||||
mount(App, { target });
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<script>
|
||||
import { ArrowLeft } from "lucide-svelte";
|
||||
|
||||
export let title = "";
|
||||
export let subtitle = "";
|
||||
</script>
|
||||
|
||||
<header class="screen-head">
|
||||
<button class="btn btn-icon btn-square" type="button" aria-label="Назад">
|
||||
<ArrowLeft size={18} />
|
||||
</button>
|
||||
<div class="center-copy">
|
||||
<h1>{title}</h1>
|
||||
<p>{subtitle}</p>
|
||||
</div>
|
||||
<span></span>
|
||||
</header>
|
||||
@@ -0,0 +1,12 @@
|
||||
<script>
|
||||
export let number = "";
|
||||
export let label = "";
|
||||
export let wide = false;
|
||||
</script>
|
||||
|
||||
<section class:wide class="preview-phone-wrap">
|
||||
<h2><span>{number}.</span> {label}</h2>
|
||||
<div class="preview-phone">
|
||||
<slot />
|
||||
</div>
|
||||
</section>
|
||||
@@ -0,0 +1,26 @@
|
||||
<script>
|
||||
import { CreditCard, Send, WalletCards } from "lucide-svelte";
|
||||
|
||||
export let methods = [];
|
||||
|
||||
const icons = [CreditCard, Send, WalletCards, WalletCards];
|
||||
|
||||
function note(index) {
|
||||
if (index === 0) return "Visa, Mastercard";
|
||||
if (index === 1) return "Быстро и удобно";
|
||||
if (index === 2) return "USDT, BTC, ETH";
|
||||
return "ЮMoney, СБП и др.";
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="method-grid">
|
||||
{#each methods as method, index}
|
||||
<div class:active={index === 1} class="method-card">
|
||||
<svelte:component this={icons[index] || WalletCards} size={18} />
|
||||
<span>
|
||||
<strong>{method.name}</strong>
|
||||
<small>{note(index)}</small>
|
||||
</span>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
@@ -0,0 +1,11 @@
|
||||
<script>
|
||||
import { Gift, Home, Settings } from "lucide-svelte";
|
||||
|
||||
export let active = "home";
|
||||
</script>
|
||||
|
||||
<nav class="bottom-nav static">
|
||||
<button class:active={active === "home"} type="button"><Home size={20} /><span>Главная</span></button>
|
||||
<button class:active={active === "invite"} type="button"><Gift size={20} /><span>Пригласить</span></button>
|
||||
<button class:active={active === "settings"} type="button"><Settings size={20} /><span>Настройки</span></button>
|
||||
</nav>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,36 @@
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
import { svelte } from "@sveltejs/vite-plugin-svelte";
|
||||
import { defineConfig } from "vite";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const templateDir = path.resolve(__dirname, "../templates");
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [svelte()],
|
||||
build: {
|
||||
outDir: templateDir,
|
||||
emptyOutDir: false,
|
||||
minify: false,
|
||||
sourcemap: false,
|
||||
cssCodeSplit: false,
|
||||
lib: {
|
||||
entry: path.resolve(__dirname, "src/main.js"),
|
||||
name: "SubscriptionWebApp",
|
||||
formats: ["iife"],
|
||||
fileName: () => "subscription_webapp.js",
|
||||
cssFileName: "subscription_webapp",
|
||||
},
|
||||
rollupOptions: {
|
||||
output: {
|
||||
assetFileNames: (assetInfo) => {
|
||||
if (assetInfo.name && assetInfo.name.endsWith(".css")) {
|
||||
return "subscription_webapp.css";
|
||||
}
|
||||
return "subscription_webapp.[name][extname]";
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
+624
-207
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@@ -5,292 +5,25 @@
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover">
|
||||
<meta name="robots" content="noindex, nofollow">
|
||||
<meta name="theme-color" content="#05070a">
|
||||
<meta name="theme-color" content="#03070b">
|
||||
<link id="app-favicon" rel="icon" href="data:," sizes="any">
|
||||
<title>Моя подписка</title>
|
||||
<title>/minishop</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
@font-face {
|
||||
font-family: 'Twemoji Country Flags';
|
||||
unicode-range: U+1F1E6-1F1FF, U+1F3F4, U+E0062-E0063, U+E0065, U+E0067, U+E006C, U+E006E, U+E0073-E0074, U+E0077, U+E007F;
|
||||
src: url('https://cdn.jsdelivr.net/npm/country-flag-emoji-polyfill@0.1/dist/TwemojiCountryFlags.woff2') format('woff2');
|
||||
font-display: swap;
|
||||
}
|
||||
</style>
|
||||
<script src="./telegram-web-app.js"></script>
|
||||
<link rel="stylesheet" href="./subscription_webapp.css">
|
||||
<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">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="loader" class="app-loader" data-i18n="loading">Загрузка...</div>
|
||||
|
||||
<main id="app" class="app hidden">
|
||||
<header class="app-header">
|
||||
<div class="app-header-title">
|
||||
<div class="brand-logo-shell hidden" data-brand-logo-shell aria-hidden="true">
|
||||
<span class="brand-logo-spinner hidden" data-brand-logo-spinner aria-hidden="true"></span>
|
||||
<img id="brand-logo" class="brand-logo hidden" data-brand-logo alt="" aria-hidden="true">
|
||||
</div>
|
||||
<div id="brand-title" class="brand-title" data-brand-title>Моя подписка</div>
|
||||
</div>
|
||||
<div class="app-header-actions">
|
||||
<div class="lang-menu">
|
||||
<button id="lang-chip" class="lang-chip" type="button" aria-haspopup="true" aria-expanded="false" aria-controls="lang-dropdown" aria-label="Language" data-action="toggle-lang-menu">
|
||||
<span id="lang-chip-flag" class="lang-chip-flag" aria-hidden="true">🇷🇺</span>
|
||||
<span id="lang-chip-label" class="lang-chip-label">RU</span>
|
||||
<svg class="lang-chip-caret" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640" aria-hidden="true">
|
||||
<path d="M297.4 470.6C309.9 483.1 330.2 483.1 342.7 470.6L534.7 278.6C547.2 266.1 547.2 245.8 534.7 233.3C522.2 220.8 501.9 220.8 489.4 233.3L320 402.7L150.6 233.4C138.1 220.9 117.8 220.9 105.3 233.4C92.8 245.9 92.8 266.2 105.3 278.7L297.3 470.7z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<div id="lang-dropdown" class="lang-dropdown hidden" role="menu"></div>
|
||||
</div>
|
||||
<div class="user-menu">
|
||||
<button id="user-chip" class="user-chip" type="button" aria-haspopup="true" aria-expanded="false" aria-controls="user-dropdown" data-action="toggle-user-menu">
|
||||
<img id="user-chip-avatar" class="user-chip-avatar" alt="" aria-hidden="true">
|
||||
<span id="user-chip-name" class="user-chip-name">...</span>
|
||||
<svg class="user-chip-caret" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640" aria-hidden="true">
|
||||
<path d="M297.4 470.6C309.9 483.1 330.2 483.1 342.7 470.6L534.7 278.6C547.2 266.1 547.2 245.8 534.7 233.3C522.2 220.8 501.9 220.8 489.4 233.3L320 402.7L150.6 233.4C138.1 220.9 117.8 220.9 105.3 233.4C92.8 245.9 92.8 266.2 105.3 278.7L297.3 470.7z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<div id="user-dropdown" class="user-dropdown hidden" role="menu">
|
||||
<div class="user-dropdown-head">
|
||||
<img id="user-dropdown-avatar" class="user-dropdown-avatar" alt="" aria-hidden="true">
|
||||
<div class="user-dropdown-head-text">
|
||||
<div id="user-dropdown-name" class="user-dropdown-name">...</div>
|
||||
<div id="user-dropdown-sub" class="user-dropdown-sub"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="user-dropdown-rows">
|
||||
<div class="user-dropdown-row" role="menuitem">
|
||||
<span class="user-dropdown-row-label" data-i18n="email_label">Email</span>
|
||||
<span id="user-dropdown-email-status" class="user-dropdown-row-status">...</span>
|
||||
</div>
|
||||
<div class="user-dropdown-row" role="menuitem">
|
||||
<span class="user-dropdown-row-label" data-i18n="telegram_label">Telegram</span>
|
||||
<span id="user-dropdown-telegram-status" class="user-dropdown-row-status">...</span>
|
||||
</div>
|
||||
</div>
|
||||
<button class="user-dropdown-logout" type="button" role="menuitem" data-action="logout">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640" aria-hidden="true">
|
||||
<path d="M224 160C241.7 160 256 145.7 256 128C256 110.3 241.7 96 224 96L160 96C107 96 64 139 64 192L64 448C64 501 107 544 160 544L224 544C241.7 544 256 529.7 256 512C256 494.3 241.7 480 224 480L160 480C142.3 480 128 465.7 128 448L128 192C128 174.3 142.3 160 160 160L224 160zM566.6 342.6C579.1 330.1 579.1 309.8 566.6 297.3L438.6 169.3C426.1 156.8 405.8 156.8 393.3 169.3C380.8 181.8 380.8 202.1 393.3 214.6L466.7 288L256 288C238.3 288 224 302.3 224 320C224 337.7 238.3 352 256 352L466.7 352L393.3 425.4C380.8 437.9 380.8 458.2 393.3 470.7C405.8 483.2 426.1 483.2 438.6 470.7L566.6 342.7z"/>
|
||||
</svg>
|
||||
<span data-i18n="logout">Выйти</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="panel grid gap-[17px] p-[18px]">
|
||||
<div id="sub-status" class="sub-status">
|
||||
<span class="sub-status-dot" aria-hidden="true"></span>
|
||||
<span id="sub-status-text" class="sub-status-text">...</span>
|
||||
</div>
|
||||
|
||||
<div class="sub-countdown">
|
||||
<div class="sub-countdown-row">
|
||||
<span id="remaining" class="sub-countdown-value">...</span>
|
||||
<span id="end-date-caption" class="sub-countdown-caption"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="traffic-bar" class="traffic-bar" role="progressbar" aria-valuemin="0" aria-valuemax="100">
|
||||
<div id="traffic-bar-fill" class="traffic-bar-fill"></div>
|
||||
<span id="traffic" class="traffic-bar-value">...</span>
|
||||
</div>
|
||||
|
||||
<div id="connect-actions">
|
||||
<button id="connect-btn" class="btn-primary w-full" type="button" data-action="open-connect-link" data-i18n="connect">Подключиться</button>
|
||||
</div>
|
||||
|
||||
<div class="mt-0.5 border-t border-[var(--border)] pt-3.5">
|
||||
<button id="extend-btn" class="btn-ghost w-full" type="button" data-action="toggle-payment-flow" data-i18n="extend_subscription">Продлить подписку/Добавить дни</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="link-panel" class="panel hidden grid gap-3.5 p-[17px]">
|
||||
<div class="panel-head">
|
||||
<div>
|
||||
<div id="link-panel-title" class="section-title" data-i18n="link_panel_title">Привяжите способ входа</div>
|
||||
<div id="link-panel-caption" class="section-caption" data-i18n="link_panel_caption">Чтобы не терять доступ к кабинету</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="email-link-box" class="hidden grid gap-2">
|
||||
<div class="link-cta-head">
|
||||
<span class="link-cta-title" data-i18n="link_email_title">Привяжите email</span>
|
||||
<span class="link-cta-caption" data-i18n="link_email_caption">Получайте код для входа на почту</span>
|
||||
</div>
|
||||
<div class="grid grid-cols-[minmax(0,1fr)_minmax(120px,auto)] gap-2">
|
||||
<input id="email-link-input" class="input w-full" type="email" autocomplete="email" inputmode="email" placeholder="mail@example.com" data-placeholder-i18n="email_placeholder">
|
||||
<button id="email-link-send-btn" class="btn" type="button" data-action="request-email-link-code" data-i18n="send_code">Отправить код</button>
|
||||
</div>
|
||||
<div id="email-link-code-row" class="hidden grid grid-cols-[minmax(0,1fr)_minmax(120px,auto)] gap-2">
|
||||
<input id="email-link-code-input" class="input input-code w-full" type="text" inputmode="numeric" autocomplete="one-time-code" maxlength="6" placeholder="000000" data-placeholder-i18n="code_placeholder">
|
||||
<button id="email-link-verify-btn" class="btn-primary" type="button" data-action="verify-email-link-code" data-i18n="confirm">Подтвердить</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="telegram-link-box" class="hidden grid gap-2">
|
||||
<div class="link-cta-head">
|
||||
<span class="link-cta-title" data-i18n="link_telegram_title">Привяжите Telegram</span>
|
||||
<span class="link-cta-caption" data-i18n="link_telegram_caption">Входите одним нажатием через Telegram</span>
|
||||
</div>
|
||||
<div id="telegram-link-widget" class="telegram-login-widget" aria-live="polite"></div>
|
||||
<div id="telegram-link-status" class="status-text hidden" aria-live="polite"></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="grid min-w-0 grid-cols-2 gap-2">
|
||||
<button class="btn w-full" type="button" data-action="open-referral-modal" data-i18n="referral_title">Пригласить друга</button>
|
||||
<button class="btn w-full" type="button" data-action="open-promo-modal" data-i18n="promo_title">Промокод</button>
|
||||
</section>
|
||||
|
||||
|
||||
|
||||
|
||||
<a id="support-link" class="hidden mt-0.5 btn" href="#" target="_blank" rel="noopener" data-i18n="support">Поддержка</a>
|
||||
<div id="legal-links-app" data-legal-links class="legal-links hidden">
|
||||
<a class="legal-link max-[560px]:flex-[1_1_100%]" data-legal-key="privacyPolicyUrl" href="#" target="_blank" rel="noopener" data-i18n="privacy_policy">Политика конфиденциальности</a>
|
||||
<a class="legal-link max-[560px]:flex-[1_1_100%]" data-legal-key="userAgreementUrl" href="#" target="_blank" rel="noopener" data-i18n="user_agreement">Пользовательское соглашение</a>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<div id="payment-modal" class="modal hidden" role="dialog" aria-modal="true" aria-labelledby="payment-title">
|
||||
<button class="modal-backdrop" type="button" data-title-i18n="close_payment" aria-label="Закрыть оплату" data-action="close-payment-flow"></button>
|
||||
<section id="payment-flow" class="panel-modal modal-card grid gap-4 p-4">
|
||||
<div class="panel-head">
|
||||
<div>
|
||||
<div id="payment-title" class="section-title text-[var(--accent)]" data-i18n="payment_title">Оплата подписки</div>
|
||||
</div>
|
||||
<button class="icon-btn" type="button" data-title-i18n="close_payment" aria-label="Закрыть оплату" data-action="close-payment-flow">×</button>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2.5">
|
||||
<div class="section-label" data-i18n="select_period">Выберите период</div>
|
||||
<div id="plans" class="grid grid-cols-2 gap-2 sm:grid-cols-4"></div>
|
||||
</div>
|
||||
|
||||
<div class="notice grid gap-1.5">
|
||||
<div class="section-label" data-i18n="payment_amount_label">Стоимость выбранного периода</div>
|
||||
<div id="selected-plan-price" class="text-[20px] font-extrabold leading-tight text-[var(--text-primary)]">...</div>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2.5">
|
||||
<div class="section-label" data-i18n="choose_payment_method">Способ оплаты</div>
|
||||
<div id="payment-methods" class="grid gap-2"></div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div id="promo-modal" class="modal hidden" role="dialog" aria-modal="true" aria-labelledby="promo-title">
|
||||
<button class="modal-backdrop" type="button" data-title-i18n="close" aria-label="Закрыть" data-action="close-promo-modal"></button>
|
||||
<section id="promo-panel" class="panel-modal modal-card grid gap-3.5 p-[17px]">
|
||||
<div class="panel-head">
|
||||
<div>
|
||||
<div id="promo-title" class="section-title text-[var(--accent)]" data-i18n="promo_title">Промокод</div>
|
||||
<div class="section-caption" data-i18n="promo_caption">Введите код, чтобы начислить бонусные дни.</div>
|
||||
</div>
|
||||
<button class="icon-btn" type="button" data-title-i18n="close" aria-label="Закрыть" data-action="close-promo-modal">×</button>
|
||||
</div>
|
||||
<div class="grid grid-cols-[minmax(0,1fr)_minmax(120px,auto)] gap-2">
|
||||
<input id="promo-code-input" class="input input-code w-full" type="text" autocomplete="off" inputmode="text" placeholder="PROMO2026" data-placeholder-i18n="promo_placeholder">
|
||||
<button id="promo-apply-btn" class="btn-primary" type="button" data-action="apply-promo-code" data-i18n="apply_promo">Применить</button>
|
||||
</div>
|
||||
<div id="promo-status" class="status-text hidden" aria-live="polite"></div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div id="referral-modal" class="modal modal--page hidden" role="dialog" aria-modal="true" aria-labelledby="referral-title">
|
||||
<button class="modal-backdrop" type="button" data-title-i18n="close" aria-label="Закрыть" data-action="close-referral-modal"></button>
|
||||
<section id="referral-panel" class="panel-modal modal-card grid gap-3.5 p-[17px]"></section>
|
||||
</div>
|
||||
|
||||
<div id="account-merge-modal" class="modal modal--page hidden" role="dialog" aria-modal="true" aria-labelledby="account-merge-title" aria-describedby="account-merge-caption">
|
||||
<button class="modal-backdrop" type="button" data-title-i18n="close" aria-label="Закрыть" data-action="close-account-merge-modal"></button>
|
||||
<section id="account-merge-panel" class="panel-modal modal-card grid gap-3.5 p-[17px]"></section>
|
||||
</div>
|
||||
|
||||
<div id="email-code-modal" class="modal auth-code-modal hidden" role="dialog" aria-modal="true" aria-labelledby="email-code-title" aria-describedby="email-code-caption">
|
||||
<button class="modal-backdrop" type="button" data-title-i18n="close" aria-label="Закрыть" data-action="close-email-login-code-modal"></button>
|
||||
<div class="relative z-[1] grid w-[min(100%,420px)] justify-items-stretch gap-2.5">
|
||||
<section class="panel-modal modal-card grid gap-3.5 p-[18px]">
|
||||
<div class="panel-head">
|
||||
<div>
|
||||
<div id="email-code-title" class="section-title" data-i18n="email_code_title">Подтвердите вход</div>
|
||||
<div id="email-code-caption" class="section-caption" data-i18n="email_code_caption">Введите 6-значный код из письма.</div>
|
||||
</div>
|
||||
<button class="icon-btn" type="button" data-title-i18n="close" aria-label="Закрыть" data-action="close-email-login-code-modal">×</button>
|
||||
</div>
|
||||
|
||||
<div class="metric rounded-[var(--radius-md)] grid-cols-[minmax(0,0.6fr)_minmax(0,1.4fr)]">
|
||||
<div class="metric-label" data-i18n="email_label">Email</div>
|
||||
<div id="email-code-address" class="metric-value">...</div>
|
||||
</div>
|
||||
|
||||
<label class="relative block min-h-14 cursor-text">
|
||||
<input id="email-login-code-input" class="otp-input" type="text" inputmode="numeric" autocomplete="one-time-code" maxlength="6" pattern="[0-9]*" data-aria-i18n="email_code_aria" aria-label="Код подтверждения">
|
||||
<span class="otp-slots" aria-hidden="true">
|
||||
<span id="email-code-slot-0"></span>
|
||||
<span id="email-code-slot-1"></span>
|
||||
<span id="email-code-slot-2"></span>
|
||||
<span id="email-code-slot-3"></span>
|
||||
<span id="email-code-slot-4"></span>
|
||||
<span id="email-code-slot-5"></span>
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<div id="email-code-status" class="status-text hidden" aria-live="polite"></div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<button id="email-login-verify-btn" class="btn-primary w-full" type="button" data-action="verify-email-login-code" data-i18n="login">Войти</button>
|
||||
</div>
|
||||
</section>
|
||||
<button id="email-login-resend-btn" class="code-modal-resend" type="button" data-action="resend-email-login-code" data-i18n="resend_code">Отправить еще раз</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section id="login" class="login login-layout gap-5">
|
||||
<div class="grid justify-items-center gap-3 text-center">
|
||||
<div class="brand-logo-shell brand-logo-shell--lg hidden" data-brand-logo-shell aria-hidden="true">
|
||||
<span class="brand-logo-spinner hidden" data-brand-logo-spinner aria-hidden="true"></span>
|
||||
<img id="login-brand-logo" class="brand-logo hidden" data-brand-logo alt="" aria-hidden="true">
|
||||
</div>
|
||||
<div id="login-brand-title" class="max-w-[320px] break-words font-[family-name:var(--font-mono)] text-[28px] font-extrabold leading-[1.05] tracking-[0] text-[var(--accent)] max-[460px]:max-w-[240px] max-[460px]:text-[24px]" data-brand-title>Моя подписка</div>
|
||||
</div>
|
||||
<div class="panel-modal w-full grid gap-5 p-6 text-center">
|
||||
<h1 class="section-title text-[22px] leading-[1.1]" data-i18n="login_title">Войдите или зарегистрируйтесь</h1>
|
||||
<div class="grid gap-2.5">
|
||||
<div class="auth-tabs-wrapper" role="tablist">
|
||||
<button id="email-auth-tab" class="auth-tab active" type="button" data-action="set-auth-mode" data-mode="email" data-i18n="email_login_tab">Email</button>
|
||||
<button id="telegram-auth-tab" class="auth-tab" type="button" data-action="set-auth-mode" data-mode="telegram" data-i18n="telegram_login_tab">Telegram</button>
|
||||
</div>
|
||||
<div class="login-auth-body">
|
||||
<div id="email-login-pane" class="grid gap-2.5">
|
||||
<div class="grid gap-2.5">
|
||||
<input id="email-login-input" class="input w-full" type="email" autocomplete="email" inputmode="email" placeholder="mail@example.com" data-placeholder-i18n="email_placeholder">
|
||||
<button id="email-login-send-btn" class="btn-primary w-full" type="button" data-action="request-email-login-code" data-i18n="login_continue">Продолжить</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="telegram-login-pane" class="hidden grid gap-2.5">
|
||||
<div id="telegram-login-widget" class="telegram-login-widget" aria-live="polite"></div>
|
||||
</div>
|
||||
<div id="auth-status" class="status-text hidden" aria-live="polite"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="legal-links-login" data-legal-links class="legal-links hidden">
|
||||
<a class="legal-link max-[560px]:flex-[1_1_100%]" data-legal-key="privacyPolicyUrl" href="#" target="_blank" rel="noopener" data-i18n="privacy_policy">Политика конфиденциальности</a>
|
||||
<a class="legal-link max-[560px]:flex-[1_1_100%]" data-legal-key="userAgreementUrl" href="#" target="_blank" rel="noopener" data-i18n="user_agreement">Пользовательское соглашение</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div id="toast" class="toast hidden" role="status" aria-live="polite"></div>
|
||||
<main id="app"></main>
|
||||
|
||||
<!-- WEBAPP_I18N_SCRIPT -->
|
||||
<!-- WEBAPP_CONFIG_SCRIPT -->
|
||||
<!-- WEBAPP_JS_SCRIPT -->
|
||||
<!-- WEBAPP_DEV_MOCK_START -->
|
||||
<script src="/subscription_webapp.js" defer></script>
|
||||
<!-- WEBAPP_DEV_MOCK_END -->
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,575 +0,0 @@
|
||||
(function(window) {
|
||||
(function(window){
|
||||
window.__parseFunction = function(__func, __attrs) {
|
||||
__attrs = __attrs || [];
|
||||
__func = '(function(' + __attrs.join(',') + '){' + __func + '})';
|
||||
return window.execScript ? window.execScript(__func) : eval(__func);
|
||||
}
|
||||
}(window));
|
||||
(function(window){
|
||||
|
||||
function addEvent(el, event, handler) {
|
||||
var events = event.split(/\s+/);
|
||||
for (var i = 0; i < events.length; i++) {
|
||||
if (el.addEventListener) {
|
||||
el.addEventListener(events[i], handler);
|
||||
} else {
|
||||
el.attachEvent('on' + events[i], handler);
|
||||
}
|
||||
}
|
||||
}
|
||||
function removeEvent(el, event, handler) {
|
||||
var events = event.split(/\s+/);
|
||||
for (var i = 0; i < events.length; i++) {
|
||||
if (el.removeEventListener) {
|
||||
el.removeEventListener(events[i], handler);
|
||||
} else {
|
||||
el.detachEvent('on' + events[i], handler);
|
||||
}
|
||||
}
|
||||
}
|
||||
function getCssProperty(el, prop) {
|
||||
if (window.getComputedStyle) {
|
||||
return window.getComputedStyle(el, '').getPropertyValue(prop) || null;
|
||||
} else if (el.currentStyle) {
|
||||
return el.currentStyle[prop] || null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function geById(el_or_id) {
|
||||
if (typeof el_or_id == 'string' || el_or_id instanceof String) {
|
||||
return document.getElementById(el_or_id);
|
||||
} else if (el_or_id instanceof HTMLElement) {
|
||||
return el_or_id;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
var getWidgetsOrigin = function(default_origin, dev_origin) {
|
||||
var link = document.createElement('A'), origin;
|
||||
link.href = document.currentScript && document.currentScript.src || default_origin;
|
||||
origin = link.origin || link.protocol + '//' + link.hostname;
|
||||
if (origin == 'https://telegram.org') {
|
||||
origin = default_origin;
|
||||
} else if (origin == 'https://telegram-js.azureedge.net' || origin == 'https://tg.dev') {
|
||||
origin = dev_origin;
|
||||
} else {
|
||||
origin = default_origin;
|
||||
}
|
||||
return origin;
|
||||
};
|
||||
|
||||
var getPageCanonical = function() {
|
||||
var a = document.createElement('A'), link, href;
|
||||
if (document.querySelector) {
|
||||
link = document.querySelector('link[rel="canonical"]');
|
||||
if (link && (href = link.getAttribute('href'))) {
|
||||
a.href = href;
|
||||
return a.href;
|
||||
}
|
||||
} else {
|
||||
var links = document.getElementsByTagName('LINK');
|
||||
for (var i = 0; i < links.length; i++) {
|
||||
if ((link = links[i]) &&
|
||||
(link.getAttribute('rel') == 'canonical') &&
|
||||
(href = link.getAttribute('href'))) {
|
||||
a.href = href;
|
||||
return a.href;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
function haveTgAuthResult() {
|
||||
var locationHash = '', re = /[#\?\&]tgAuthResult=([A-Za-z0-9\-_=]*)$/, match;
|
||||
try {
|
||||
locationHash = location.hash.toString();
|
||||
if (match = locationHash.match(re)) {
|
||||
location.hash = locationHash.replace(re, '');
|
||||
var data = match[1] || '';
|
||||
data = data.replace(/-/g, '+').replace(/_/g, '/');
|
||||
var pad = data.length % 4;
|
||||
if (pad > 1) {
|
||||
data += new Array(5 - pad).join('=');
|
||||
}
|
||||
return JSON.parse(window.atob(data));
|
||||
}
|
||||
} catch (e) {}
|
||||
return false;
|
||||
}
|
||||
|
||||
function getXHR() {
|
||||
if (navigator.appName == "Microsoft Internet Explorer"){
|
||||
return new ActiveXObject("Microsoft.XMLHTTP");
|
||||
} else {
|
||||
return new XMLHttpRequest();
|
||||
}
|
||||
}
|
||||
|
||||
if (!window.Telegram) {
|
||||
window.Telegram = {};
|
||||
}
|
||||
if (!window.Telegram.__WidgetUuid) {
|
||||
window.Telegram.__WidgetUuid = 0;
|
||||
}
|
||||
if (!window.Telegram.__WidgetLastId) {
|
||||
window.Telegram.__WidgetLastId = 0;
|
||||
}
|
||||
if (!window.Telegram.__WidgetCallbacks) {
|
||||
window.Telegram.__WidgetCallbacks = {};
|
||||
}
|
||||
|
||||
function postMessageToIframe(iframe, event, data, callback) {
|
||||
if (!iframe._ready) {
|
||||
if (!iframe._readyQueue) iframe._readyQueue = [];
|
||||
iframe._readyQueue.push([event, data, callback]);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
data = data || {};
|
||||
data.event = event;
|
||||
if (callback) {
|
||||
data._cb = ++window.Telegram.__WidgetLastId;
|
||||
window.Telegram.__WidgetCallbacks[data._cb] = {
|
||||
iframe: iframe,
|
||||
callback: callback
|
||||
};
|
||||
}
|
||||
iframe.contentWindow.postMessage(JSON.stringify(data), '*');
|
||||
} catch(e) {}
|
||||
}
|
||||
|
||||
function initWidget(widgetEl) {
|
||||
var widgetId, widgetElId, widgetsOrigin, existsEl,
|
||||
src, styles = {}, allowedAttrs = [],
|
||||
defWidth, defHeight, scrollable = false, onInitAuthUser, onAuthUser, onUnauth;
|
||||
if (!widgetEl.tagName ||
|
||||
!(widgetEl.tagName.toUpperCase() == 'SCRIPT' ||
|
||||
widgetEl.tagName.toUpperCase() == 'BLOCKQUOTE' &&
|
||||
widgetEl.classList.contains('telegram-post'))) {
|
||||
return null;
|
||||
}
|
||||
if (widgetEl._iframe) {
|
||||
return widgetEl._iframe;
|
||||
}
|
||||
if (widgetId = widgetEl.getAttribute('data-telegram-post')) {
|
||||
var comment = widgetEl.getAttribute('data-comment') || '';
|
||||
widgetsOrigin = getWidgetsOrigin('https://t.me', 'https://post.tg.dev');
|
||||
widgetElId = 'telegram-post-' + widgetId.replace(/[^a-z0-9_]/ig, '-') + (comment ? '-comment' + comment : '');
|
||||
src = widgetsOrigin + '/' + widgetId + '?embed=1';
|
||||
allowedAttrs = ['comment', 'userpic', 'mode', 'single?', 'color', 'dark', 'dark_color'];
|
||||
defWidth = widgetEl.getAttribute('data-width') || '100%';
|
||||
defHeight = '';
|
||||
styles.minWidth = '320px';
|
||||
}
|
||||
else if (widgetId = widgetEl.getAttribute('data-telegram-discussion')) {
|
||||
widgetsOrigin = getWidgetsOrigin('https://t.me', 'https://post.tg.dev');
|
||||
widgetElId = 'telegram-discussion-' + widgetId.replace(/[^a-z0-9_]/ig, '-') + '-' + (++window.Telegram.__WidgetUuid);
|
||||
var websitePageUrl = widgetEl.getAttribute('data-page-url');
|
||||
if (!websitePageUrl) {
|
||||
websitePageUrl = getPageCanonical();
|
||||
}
|
||||
src = widgetsOrigin + '/' + widgetId + '?embed=1&discussion=1' + (websitePageUrl ? '&page_url=' + encodeURIComponent(websitePageUrl) : '');
|
||||
allowedAttrs = ['comments_limit', 'color', 'colorful', 'dark', 'dark_color', 'width', 'height'];
|
||||
defWidth = widgetEl.getAttribute('data-width') || '100%';
|
||||
defHeight = widgetEl.getAttribute('data-height') || 0;
|
||||
styles.minWidth = '320px';
|
||||
if (defHeight > 0) {
|
||||
scrollable = true;
|
||||
}
|
||||
}
|
||||
else if (widgetEl.hasAttribute('data-telegram-login')) {
|
||||
widgetId = widgetEl.getAttribute('data-telegram-login');
|
||||
widgetsOrigin = getWidgetsOrigin('https://oauth.telegram.org', 'https://oauth.tg.dev');
|
||||
widgetElId = 'telegram-login-' + widgetId.replace(/[^a-z0-9_]/ig, '-');
|
||||
src = widgetsOrigin + '/embed/' + widgetId + '?origin=' + encodeURIComponent(location.origin || location.protocol + '//' + location.hostname) + '&return_to=' + encodeURIComponent(location.href);
|
||||
allowedAttrs = ['size', 'userpic', 'init_auth', 'request_access', 'radius', 'min_width', 'max_width', 'lang'];
|
||||
defWidth = 186;
|
||||
defHeight = 28;
|
||||
if (widgetEl.hasAttribute('data-size')) {
|
||||
var size = widgetEl.getAttribute('data-size');
|
||||
if (size == 'small') defWidth = 148, defHeight = 20;
|
||||
else if (size == 'large') defWidth = 238, defHeight = 40;
|
||||
}
|
||||
if (widgetEl.hasAttribute('data-onauth')) {
|
||||
onInitAuthUser = onAuthUser = __parseFunction(widgetEl.getAttribute('data-onauth'), ['user']);
|
||||
}
|
||||
else if (widgetEl.hasAttribute('data-auth-url')) {
|
||||
var a = document.createElement('A');
|
||||
a.href = widgetEl.getAttribute('data-auth-url');
|
||||
onAuthUser = function(user) {
|
||||
var authUrl = a.href;
|
||||
authUrl += (authUrl.indexOf('?') >= 0) ? '&' : '?';
|
||||
var params = [];
|
||||
for (var key in user) {
|
||||
params.push(key + '=' + encodeURIComponent(user[key]));
|
||||
}
|
||||
authUrl += params.join('&');
|
||||
location.href = authUrl;
|
||||
};
|
||||
}
|
||||
if (widgetEl.hasAttribute('data-onunauth')) {
|
||||
onUnauth = __parseFunction(widgetEl.getAttribute('data-onunauth'));
|
||||
}
|
||||
var auth_result = haveTgAuthResult();
|
||||
if (auth_result && onAuthUser) {
|
||||
onAuthUser(auth_result);
|
||||
}
|
||||
}
|
||||
else if (widgetId = widgetEl.getAttribute('data-telegram-share-url')) {
|
||||
widgetsOrigin = getWidgetsOrigin('https://t.me', 'https://post.tg.dev');
|
||||
widgetElId = 'telegram-share-' + window.btoa(widgetId);
|
||||
src = widgetsOrigin + '/share/embed?origin=' + encodeURIComponent(location.origin || location.protocol + '//' + location.hostname);
|
||||
allowedAttrs = ['telegram-share-url', 'comment', 'size', 'text'];
|
||||
defWidth = 60;
|
||||
defHeight = 20;
|
||||
if (widgetEl.getAttribute('data-size') == 'large') {
|
||||
defWidth = 76;
|
||||
defHeight = 28;
|
||||
}
|
||||
}
|
||||
else {
|
||||
return null;
|
||||
}
|
||||
existsEl = document.getElementById(widgetElId);
|
||||
if (existsEl) {
|
||||
return existsEl;
|
||||
}
|
||||
for (var i = 0; i < allowedAttrs.length; i++) {
|
||||
var attr = allowedAttrs[i];
|
||||
var novalue = attr.substr(-1) == '?';
|
||||
if (novalue) {
|
||||
attr = attr.slice(0, -1);
|
||||
}
|
||||
var data_attr = 'data-' + attr.replace(/_/g, '-');
|
||||
if (widgetEl.hasAttribute(data_attr)) {
|
||||
var attr_value = novalue ? '1' : encodeURIComponent(widgetEl.getAttribute(data_attr));
|
||||
src += '&' + attr + '=' + attr_value;
|
||||
}
|
||||
}
|
||||
function getCurCoords(iframe) {
|
||||
var docEl = document.documentElement;
|
||||
var frect = iframe.getBoundingClientRect();
|
||||
return {
|
||||
frameTop: frect.top,
|
||||
frameBottom: frect.bottom,
|
||||
frameLeft: frect.left,
|
||||
frameRight: frect.right,
|
||||
frameWidth: frect.width,
|
||||
frameHeight: frect.height,
|
||||
scrollTop: window.pageYOffset,
|
||||
scrollLeft: window.pageXOffset,
|
||||
clientWidth: docEl.clientWidth,
|
||||
clientHeight: docEl.clientHeight
|
||||
};
|
||||
}
|
||||
function visibilityHandler() {
|
||||
if (isVisible(iframe, 50)) {
|
||||
postMessageToIframe(iframe, 'visible', {frame: widgetElId});
|
||||
}
|
||||
}
|
||||
function focusHandler() {
|
||||
postMessageToIframe(iframe, 'focus', {has_focus: document.hasFocus()});
|
||||
}
|
||||
function postMessageHandler(event) {
|
||||
if (event.source !== iframe.contentWindow ||
|
||||
event.origin != widgetsOrigin) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
var data = JSON.parse(event.data);
|
||||
} catch(e) {
|
||||
var data = {};
|
||||
}
|
||||
if (data.event == 'resize') {
|
||||
if (data.height) {
|
||||
iframe.style.height = data.height + 'px';
|
||||
}
|
||||
if (data.width) {
|
||||
iframe.style.width = data.width + 'px';
|
||||
}
|
||||
}
|
||||
else if (data.event == 'ready') {
|
||||
iframe._ready = true;
|
||||
focusHandler();
|
||||
for (var i = 0; i < iframe._readyQueue.length; i++) {
|
||||
var queue_item = iframe._readyQueue[i];
|
||||
postMessageToIframe(iframe, queue_item[0], queue_item[1], queue_item[2]);
|
||||
}
|
||||
iframe._readyQueue = [];
|
||||
}
|
||||
else if (data.event == 'visible_off') {
|
||||
removeEvent(window, 'scroll', visibilityHandler);
|
||||
removeEvent(window, 'resize', visibilityHandler);
|
||||
}
|
||||
else if (data.event == 'get_coords') {
|
||||
postMessageToIframe(iframe, 'callback', {
|
||||
_cb: data._cb,
|
||||
value: getCurCoords(iframe)
|
||||
});
|
||||
}
|
||||
else if (data.event == 'scroll_to') {
|
||||
try {
|
||||
window.scrollTo(data.x || 0, data.y || 0);
|
||||
} catch(e) {}
|
||||
}
|
||||
else if (data.event == 'auth_user') {
|
||||
if (data.init) {
|
||||
onInitAuthUser && onInitAuthUser(data.auth_data);
|
||||
} else {
|
||||
onAuthUser && onAuthUser(data.auth_data);
|
||||
}
|
||||
}
|
||||
else if (data.event == 'unauthorized') {
|
||||
onUnauth && onUnauth();
|
||||
}
|
||||
else if (data.event == 'callback') {
|
||||
var cb_data = null;
|
||||
if (cb_data = window.Telegram.__WidgetCallbacks[data._cb]) {
|
||||
if (cb_data.iframe === iframe) {
|
||||
cb_data.callback(data.value);
|
||||
delete window.Telegram.__WidgetCallbacks[data._cb];
|
||||
}
|
||||
} else {
|
||||
console.warn('Callback #' + data._cb + ' not found');
|
||||
}
|
||||
}
|
||||
}
|
||||
var iframe = document.createElement('iframe');
|
||||
iframe.id = widgetElId;
|
||||
iframe.src = src;
|
||||
iframe.width = defWidth;
|
||||
iframe.height = defHeight;
|
||||
iframe.setAttribute('frameborder', '0');
|
||||
if (!scrollable) {
|
||||
iframe.setAttribute('scrolling', 'no');
|
||||
iframe.style.overflow = 'hidden';
|
||||
}
|
||||
iframe.style.colorScheme = 'light dark';
|
||||
iframe.style.border = 'none';
|
||||
for (var prop in styles) {
|
||||
iframe.style[prop] = styles[prop];
|
||||
}
|
||||
if (widgetEl.parentNode) {
|
||||
widgetEl.parentNode.insertBefore(iframe, widgetEl);
|
||||
if (widgetEl.tagName.toUpperCase() == 'BLOCKQUOTE') {
|
||||
widgetEl.parentNode.removeChild(widgetEl);
|
||||
}
|
||||
}
|
||||
iframe._ready = false;
|
||||
iframe._readyQueue = [];
|
||||
widgetEl._iframe = iframe;
|
||||
addEvent(iframe, 'load', function() {
|
||||
removeEvent(iframe, 'load', visibilityHandler);
|
||||
addEvent(window, 'scroll', visibilityHandler);
|
||||
addEvent(window, 'resize', visibilityHandler);
|
||||
visibilityHandler();
|
||||
});
|
||||
addEvent(window, 'focus blur', focusHandler);
|
||||
addEvent(window, 'message', postMessageHandler);
|
||||
return iframe;
|
||||
}
|
||||
function isVisible(el, padding) {
|
||||
var node = el, val;
|
||||
var visibility = getCssProperty(node, 'visibility');
|
||||
if (visibility == 'hidden') return false;
|
||||
while (node) {
|
||||
if (node === document.documentElement) break;
|
||||
var display = getCssProperty(node, 'display');
|
||||
if (display == 'none') return false;
|
||||
var opacity = getCssProperty(node, 'opacity');
|
||||
if (opacity !== null && opacity < 0.1) return false;
|
||||
node = node.parentNode;
|
||||
}
|
||||
if (el.getBoundingClientRect) {
|
||||
padding = +padding || 0;
|
||||
var rect = el.getBoundingClientRect();
|
||||
var html = document.documentElement;
|
||||
if (rect.bottom < padding ||
|
||||
rect.right < padding ||
|
||||
rect.top > (window.innerHeight || html.clientHeight) - padding ||
|
||||
rect.left > (window.innerWidth || html.clientWidth) - padding) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function getAllWidgets() {
|
||||
var widgets = [];
|
||||
if (document.querySelectorAll) {
|
||||
widgets = document.querySelectorAll('script[data-telegram-post],blockquote.telegram-post,script[data-telegram-discussion],script[data-telegram-login],script[data-telegram-share-url]');
|
||||
} else {
|
||||
widgets = Array.prototype.slice.apply(document.getElementsByTagName('SCRIPT'));
|
||||
widgets = widgets.concat(Array.prototype.slice.apply(document.getElementsByTagName('BLOCKQUOTE')));
|
||||
}
|
||||
return widgets;
|
||||
}
|
||||
|
||||
function getWidgetInfo(el_or_id, callback) {
|
||||
var e = null, iframe = null;
|
||||
if (el = geById(el_or_id)) {
|
||||
if (el.tagName &&
|
||||
el.tagName.toUpperCase() == 'IFRAME') {
|
||||
iframe = el;
|
||||
} else if (el._iframe) {
|
||||
iframe = el._iframe;
|
||||
}
|
||||
if (iframe && callback) {
|
||||
postMessageToIframe(iframe, 'get_info', {}, callback);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setWidgetOptions(options, el_or_id) {
|
||||
var e = null, iframe = null;
|
||||
if (typeof el_or_id === 'undefined') {
|
||||
var widgets = getAllWidgets();
|
||||
for (var i = 0; i < widgets.length; i++) {
|
||||
if (iframe = widgets[i]._iframe) {
|
||||
postMessageToIframe(iframe, 'set_options', {options: options});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (el = geById(el_or_id)) {
|
||||
if (el.tagName &&
|
||||
el.tagName.toUpperCase() == 'IFRAME') {
|
||||
iframe = el;
|
||||
} else if (el._iframe) {
|
||||
iframe = el._iframe;
|
||||
}
|
||||
if (iframe) {
|
||||
postMessageToIframe(iframe, 'set_options', {options: options});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!document.currentScript ||
|
||||
!initWidget(document.currentScript)) {
|
||||
var widgets = getAllWidgets();
|
||||
for (var i = 0; i < widgets.length; i++) {
|
||||
initWidget(widgets[i]);
|
||||
}
|
||||
}
|
||||
|
||||
var TelegramLogin = {
|
||||
popups: {},
|
||||
options: null,
|
||||
auth_callback: null,
|
||||
_init: function(options, auth_callback) {
|
||||
TelegramLogin.options = options;
|
||||
TelegramLogin.auth_callback = auth_callback;
|
||||
var auth_result = haveTgAuthResult();
|
||||
if (auth_result && auth_callback) {
|
||||
auth_callback(auth_result);
|
||||
}
|
||||
},
|
||||
_open: function(callback) {
|
||||
TelegramLogin._auth(TelegramLogin.options, function(authData) {
|
||||
if (TelegramLogin.auth_callback) {
|
||||
TelegramLogin.auth_callback(authData);
|
||||
}
|
||||
if (callback) {
|
||||
callback(authData);
|
||||
}
|
||||
});
|
||||
},
|
||||
_auth: function(options, callback) {
|
||||
var bot_id = parseInt(options.bot_id);
|
||||
if (!bot_id) {
|
||||
throw new Error('Bot id required');
|
||||
}
|
||||
var width = 550;
|
||||
var height = 470;
|
||||
var left = Math.max(0, (screen.width - width) / 2) + (screen.availLeft | 0),
|
||||
top = Math.max(0, (screen.height - height) / 2) + (screen.availTop | 0);
|
||||
var onMessage = function (event) {
|
||||
try {
|
||||
var data = JSON.parse(event.data);
|
||||
} catch(e) {
|
||||
var data = {};
|
||||
}
|
||||
if (!TelegramLogin.popups[bot_id]) return;
|
||||
if (event.source !== TelegramLogin.popups[bot_id].window) return;
|
||||
if (data.event == 'auth_result') {
|
||||
onAuthDone(data.result);
|
||||
}
|
||||
};
|
||||
var onAuthDone = function (authData) {
|
||||
if (!TelegramLogin.popups[bot_id]) return;
|
||||
if (TelegramLogin.popups[bot_id].authFinished) return;
|
||||
callback && callback(authData);
|
||||
TelegramLogin.popups[bot_id].authFinished = true;
|
||||
removeEvent(window, 'message', onMessage);
|
||||
};
|
||||
var checkClose = function(bot_id) {
|
||||
if (!TelegramLogin.popups[bot_id]) return;
|
||||
if (!TelegramLogin.popups[bot_id].window ||
|
||||
TelegramLogin.popups[bot_id].window.closed) {
|
||||
return TelegramLogin.getAuthData(options, function(origin, authData) {
|
||||
onAuthDone(authData);
|
||||
});
|
||||
}
|
||||
setTimeout(checkClose, 100, bot_id);
|
||||
}
|
||||
var popup_url = Telegram.Login.widgetsOrigin + '/auth?bot_id=' + encodeURIComponent(options.bot_id) + '&origin=' + encodeURIComponent(location.origin || location.protocol + '//' + location.hostname) + (options.request_access ? '&request_access=' + encodeURIComponent(options.request_access) : '') + (options.lang ? '&lang=' + encodeURIComponent(options.lang) : '') + '&return_to=' + encodeURIComponent(location.href);
|
||||
var popup = window.open(popup_url, 'telegram_oauth_bot' + bot_id, 'width=' + width + ',height=' + height + ',left=' + left + ',top=' + top + ',status=0,location=0,menubar=0,toolbar=0');
|
||||
TelegramLogin.popups[bot_id] = {
|
||||
window: popup,
|
||||
authFinished: false
|
||||
};
|
||||
if (popup) {
|
||||
addEvent(window, 'message', onMessage);
|
||||
popup.focus();
|
||||
checkClose(bot_id);
|
||||
}
|
||||
},
|
||||
getAuthData: function(options, callback) {
|
||||
var bot_id = parseInt(options.bot_id);
|
||||
if (!bot_id) {
|
||||
throw new Error('Bot id required');
|
||||
}
|
||||
var xhr = getXHR();
|
||||
var url = Telegram.Login.widgetsOrigin + '/auth/get';
|
||||
xhr.open('POST', url);
|
||||
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8');
|
||||
xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
|
||||
xhr.onreadystatechange = function() {
|
||||
if (xhr.readyState == 4) {
|
||||
if (typeof xhr.responseBody == 'undefined' && xhr.responseText) {
|
||||
try {
|
||||
var result = JSON.parse(xhr.responseText);
|
||||
} catch(e) {
|
||||
var result = {};
|
||||
}
|
||||
if (result.user) {
|
||||
callback(result.origin, result.user);
|
||||
} else {
|
||||
callback(result.origin, false);
|
||||
}
|
||||
} else {
|
||||
callback('*', false);
|
||||
}
|
||||
}
|
||||
};
|
||||
xhr.onerror = function() {
|
||||
callback('*', false);
|
||||
};
|
||||
xhr.withCredentials = true;
|
||||
xhr.send('bot_id=' + encodeURIComponent(options.bot_id) + (options.lang ? '&lang=' + encodeURIComponent(options.lang) : ''));
|
||||
}
|
||||
};
|
||||
|
||||
window.Telegram.getWidgetInfo = getWidgetInfo;
|
||||
window.Telegram.setWidgetOptions = setWidgetOptions;
|
||||
window.Telegram.Login = {
|
||||
init: TelegramLogin._init,
|
||||
open: TelegramLogin._open,
|
||||
auth: TelegramLogin._auth,
|
||||
widgetsOrigin: getWidgetsOrigin('https://oauth.telegram.org', 'https://oauth.tg.dev')
|
||||
};
|
||||
|
||||
}(window));
|
||||
})(window);
|
||||
@@ -1,7 +1,6 @@
|
||||
import hmac
|
||||
import asyncio
|
||||
import logging
|
||||
from contextlib import suppress
|
||||
|
||||
from aiohttp import web
|
||||
from aiogram import Bot, Dispatcher
|
||||
@@ -17,8 +16,6 @@ class SecureSimpleRequestHandler(SimpleRequestHandler):
|
||||
return False
|
||||
return hmac.compare_digest(telegram_secret_token, self.secret_token)
|
||||
|
||||
TELEGRAM_WEB_APP_SDK_REFRESH_INTERVAL_SECONDS = 24 * 60 * 60
|
||||
|
||||
|
||||
def _inject_shared_instances(
|
||||
app: web.Application,
|
||||
@@ -132,13 +129,8 @@ async def build_and_start_web_app(
|
||||
f"AIOHTTP server started on http://{settings.WEB_SERVER_HOST}:{settings.WEB_SERVER_PORT}"
|
||||
)
|
||||
|
||||
telegram_web_app_sdk_refresh_task = None
|
||||
if settings.WEBAPP_ENABLED:
|
||||
from bot.app.web.subscription_webapp import (
|
||||
create_subscription_webapp_application,
|
||||
refresh_telegram_login_widget_sdk,
|
||||
refresh_telegram_web_app_sdk,
|
||||
)
|
||||
from bot.app.web.subscription_webapp import create_subscription_webapp_application
|
||||
|
||||
subscription_app = create_subscription_webapp_application(
|
||||
dp,
|
||||
@@ -161,25 +153,9 @@ async def build_and_start_web_app(
|
||||
settings.WEBAPP_SERVER_PORT,
|
||||
)
|
||||
|
||||
async def _refresh_telegram_web_assets_forever() -> None:
|
||||
while True:
|
||||
await refresh_telegram_web_app_sdk()
|
||||
await refresh_telegram_login_widget_sdk()
|
||||
await asyncio.sleep(TELEGRAM_WEB_APP_SDK_REFRESH_INTERVAL_SECONDS)
|
||||
|
||||
telegram_web_app_sdk_refresh_task = asyncio.create_task(
|
||||
_refresh_telegram_web_assets_forever(),
|
||||
name="TelegramWebAssetsRefreshTask",
|
||||
)
|
||||
|
||||
try:
|
||||
await asyncio.Event().wait()
|
||||
finally:
|
||||
if telegram_web_app_sdk_refresh_task is not None:
|
||||
telegram_web_app_sdk_refresh_task.cancel()
|
||||
with suppress(asyncio.CancelledError):
|
||||
await telegram_web_app_sdk_refresh_task
|
||||
|
||||
for runner in reversed(runners):
|
||||
try:
|
||||
await runner.cleanup()
|
||||
|
||||
@@ -99,7 +99,7 @@ def _layout(
|
||||
logo_block = (
|
||||
f'<img src="{html.escape(logo_url, quote=True)}" width="64" height="64" '
|
||||
f'alt="" style="display:block;border:0;outline:none;text-decoration:none;'
|
||||
f'border-radius:16px;background:{_CARD_BG};">'
|
||||
f'border-radius:16px;">'
|
||||
)
|
||||
|
||||
return f"""<!DOCTYPE html>
|
||||
|
||||
@@ -495,6 +495,7 @@ class SubscriptionService:
|
||||
"is_active": True,
|
||||
"status_from_panel": "TRIAL",
|
||||
"traffic_limit_bytes": self.settings.trial_traffic_limit_bytes,
|
||||
"traffic_limit_strategy": self.settings.TRIAL_TRAFFIC_STRATEGY,
|
||||
"auto_renew_enabled": False,
|
||||
}
|
||||
try:
|
||||
@@ -516,6 +517,7 @@ class SubscriptionService:
|
||||
expire_at=end_date,
|
||||
status="ACTIVE",
|
||||
traffic_limit_bytes=self.settings.trial_traffic_limit_bytes,
|
||||
traffic_limit_strategy=self.settings.TRIAL_TRAFFIC_STRATEGY,
|
||||
)
|
||||
|
||||
panel_update_payload.update(self._panel_identity_payload_for_user(db_user))
|
||||
|
||||
Reference in New Issue
Block a user