Этап 0: документация и макеты
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
No files matched your search
@@ -0,0 +1,38 @@
|
||||
# Veilix — правила для сессий Claude
|
||||
|
||||
Сервис продажи VPN двух брендов на одном бэкенде: Вейликс (dedicated, veilix.app) и ВейликсВПН (mass, veilixvpn.com). Карта документации — [docs/00-index.md](docs/00-index.md).
|
||||
|
||||
## 1. Порядок работы в сессии
|
||||
|
||||
1. Прочитай этот файл, затем [docs/00-index.md](docs/00-index.md) и файл текущего этапа в [docs/stages/](docs/stages/). Остальные документы — по ссылкам из файла этапа.
|
||||
2. Перед началом работ сверь с пользователем план этапа: что делаем, что не делаем, какие вопросы открыты. Код не пишем, пока план этапа не подтверждён.
|
||||
3. Нашлось противоречие между документами, макетом и задачей — сначала обсуждаем, потом делаем.
|
||||
4. Есть отраслевой стандарт лучше предложенного решения или решение получается кастомным — обсуди с пользователем.
|
||||
5. В конце этапа обнови документацию: статус этапа в [docs/00-index.md](docs/00-index.md), решения в [docs/decisions/](docs/decisions/), изменения модели данных в [docs/03-data-model.md](docs/03-data-model.md).
|
||||
|
||||
## 2. Общение
|
||||
|
||||
1. Язык — русский.
|
||||
2. Нумерация пунктов в чате и в документации — только цифры и латинские буквы (1, 2, 3 и a, b, c). Кириллические буквы как маркеры не используем: не путаются латинская A и русская А, латинская B и русская В.
|
||||
|
||||
## 3. Сервер и права
|
||||
|
||||
1. Всё, что требует root (пакеты, nginx, сертификаты, роли PostgreSQL, Postfix, DNS, `loginctl`), делает пользователь. Сформулируй, что и зачем нужно сделать, и жди. Обходные пути внутри своего пользователя не ищем.
|
||||
2. Окружения на одном сервере, под разными пользователями ОС:
|
||||
a. dev — пользователь vx_dev (под ним работают сессии Claude): `/home/vx_dev/veilix_dev` (рабочая копия git), база `veilix_dev`, домены `dev.*`, порт 3100;
|
||||
b. prod — пользователь vx_prod: `/home/vx_prod/veilix` (релизы и симлинк `current`), база `veilix_prod`, основные домены, порт 3000. У vx_dev доступа к prod нет.
|
||||
3. Выкладка в prod — только по явной команде пользователя в текущей сессии и только командой выкладки ([docs/02-architecture.md](docs/02-architecture.md), раздел 5).
|
||||
|
||||
## 4. Технические правила
|
||||
|
||||
1. Стек и версии — [docs/02-architecture.md](docs/02-architecture.md). Payload 3.x; код пишем совместимо с Payload 4: в Local API всегда явно указываем `overrideAccess`.
|
||||
2. ID — только UUID (`idType: 'uuid'`). Наружу не отдаём последовательных номеров: номера заказов, ключей и реферальные коды — случайные.
|
||||
3. Деньги — целые числа в минимальных единицах (копейки, центы) плюс код валюты. Никаких float.
|
||||
4. Секреты — только в `.env` вне git.
|
||||
5. Тексты интерфейса — через next-intl, контент — локализованные поля Payload. Строк на одном языке в коде компонентов нет.
|
||||
6. Макеты — в [design/handoff/project/](design/handoff/project/). Воспроизводим их по коду макетов (разметка, CSS-переменные), а не по скриншотам.
|
||||
7. Коммиты и push в Gitea — по просьбе пользователя.
|
||||
|
||||
## 5. Команды
|
||||
|
||||
Заполняется на этапе 1a.
|
||||
@@ -0,0 +1,7 @@
|
||||
# Veilix
|
||||
|
||||
Сайты, личные кабинеты, Telegram MiniApp и боты двух VPN-брендов на одном бэкенде (Next.js + Payload CMS + PostgreSQL).
|
||||
|
||||
- Документация: [docs/00-index.md](docs/00-index.md)
|
||||
- Правила для сессий Claude: [CLAUDE.md](CLAUDE.md)
|
||||
- Макеты: [design/handoff/](design/handoff/)
|
||||
@@ -0,0 +1,22 @@
|
||||
# CODING AGENTS: READ THIS FIRST
|
||||
|
||||
This is a **handoff bundle** from Claude Design (claude.ai/design).
|
||||
|
||||
A user mocked up designs in HTML/CSS/JS using an AI design tool, then exported this bundle so a coding agent can implement the designs for real.
|
||||
|
||||
## What you should do — IMPORTANT
|
||||
|
||||
**Read `veilix/project/AppTG.dc.html` in full.** The user had this file open when they triggered the handoff, so it's almost certainly the primary design they want built. Read it top to bottom — don't skim. Then **follow its imports**: open every file it pulls in (shared components, CSS, scripts) so you understand how the pieces fit together before you start implementing.
|
||||
|
||||
**If anything is ambiguous, ask the user to confirm before you start implementing.** It's much cheaper to clarify scope up front than to build the wrong thing.
|
||||
|
||||
## About the design files
|
||||
|
||||
The design medium is **HTML/CSS/JS** — these are prototypes, not production code. Your job is to **recreate them pixel-perfectly** in whatever technology makes sense for the target codebase (React, Vue, native, whatever fits). Match the visual output; don't copy the prototype's internal structure unless it happens to fit.
|
||||
|
||||
**Don't render these files in a browser or take screenshots unless the user asks you to.** Everything you need — dimensions, colors, layout rules — is spelled out in the source. Read the HTML and CSS directly; a screenshot won't tell you anything they don't.
|
||||
|
||||
## Bundle contents
|
||||
|
||||
- `veilix/README.md` — this file
|
||||
- `veilix/project/` — the `VEILIX` project files (HTML prototypes, assets, components)
|
||||
|
After Width: | Height: | Size: 19 KiB |
@@ -0,0 +1,991 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<script src="./support.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<x-dc>
|
||||
<helmet>
|
||||
<link rel="icon" type="image/svg+xml" href="favicon.svg" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin="anonymous" />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Commissioner:wght@300;400;500;600&family=Unbounded:wght@500;600&display=swap" rel="stylesheet" />
|
||||
<script>(function(){if(window.__veilixI18nSrc)return;window.__veilixI18nSrc="./i18n.js?v="+Date.now().toString(36)+Math.random().toString(36).slice(2,8);var s=document.createElement("script");s.src=window.__veilixI18nSrc;document.head.appendChild(s);})();</script>
|
||||
<style>
|
||||
:root{
|
||||
--bg: oklch(99% 0.005 296);
|
||||
--surface: oklch(100% 0 0);
|
||||
--surface-2: oklch(97.5% 0.01 296);
|
||||
--ink: oklch(24% 0.04 296);
|
||||
--ink-2: oklch(45% 0.03 296);
|
||||
--ink-3: oklch(60% 0.025 296);
|
||||
--border: oklch(92% 0.014 296);
|
||||
--accent: oklch(45% 0.17 296);
|
||||
--accent-hover: oklch(38% 0.17 296);
|
||||
--accent-soft: oklch(95.5% 0.028 296);
|
||||
--on-accent: oklch(99% 0.01 296);
|
||||
--glow-1: oklch(86% 0.1 296);
|
||||
--glow-2: oklch(89% 0.09 266);
|
||||
--glow-3: oklch(91% 0.08 332);
|
||||
--veil: oklch(100% 0 0 / 0.55);
|
||||
--warn: oklch(62% 0.14 62);
|
||||
--shadow: 0 1px 2px oklch(24% 0.04 296 / 0.05), 0 12px 32px -12px oklch(24% 0.04 296 / 0.14);
|
||||
}
|
||||
:root[data-veilix-theme="dark"]{
|
||||
--bg: oklch(17% 0.022 296);
|
||||
--surface: oklch(21% 0.026 296);
|
||||
--surface-2: oklch(24% 0.03 296);
|
||||
--ink: oklch(96% 0.01 296);
|
||||
--ink-2: oklch(79% 0.02 296);
|
||||
--ink-3: oklch(63% 0.025 296);
|
||||
--border: oklch(31% 0.032 296);
|
||||
--accent: oklch(74% 0.15 296);
|
||||
--accent-hover: oklch(80% 0.13 296);
|
||||
--accent-soft: oklch(27% 0.05 296);
|
||||
--on-accent: oklch(17% 0.03 296);
|
||||
--glow-1: oklch(45% 0.16 296);
|
||||
--glow-2: oklch(40% 0.14 266);
|
||||
--glow-3: oklch(42% 0.13 332);
|
||||
--veil: oklch(100% 0 0 / 0.07);
|
||||
--warn: oklch(78% 0.13 62);
|
||||
--shadow: 0 1px 2px oklch(10% 0.02 296 / 0.4), 0 16px 40px -16px oklch(8% 0.02 296 / 0.7);
|
||||
}
|
||||
*{ box-sizing: border-box; }
|
||||
input, textarea, select{ font-size: 16px; }
|
||||
[hidden]{ display: none !important; }
|
||||
body{ margin:0; background: var(--bg); color: var(--ink); font-family:"Commissioner", system-ui, sans-serif; -webkit-font-smoothing: antialiased; }
|
||||
a{ color: var(--accent); text-decoration: none; }
|
||||
a:hover{ color: var(--accent-hover); }
|
||||
input::placeholder{ color: var(--ink-3); }
|
||||
</style>
|
||||
</helmet>
|
||||
|
||||
<template id="__bundler_thumbnail" data-bg-color="#efeaf8">
|
||||
<svg viewBox="0 0 1200 800" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="480" y="290" width="150" height="150" rx="48" fill="#5b32c9" />
|
||||
<rect x="570" y="360" width="150" height="150" rx="48" fill="#ffffff" fill-opacity="0.62" stroke="#5b32c9" stroke-width="6" />
|
||||
</svg>
|
||||
</template>
|
||||
<div style="background: var(--bg); color: var(--ink); font-family: 'Commissioner', system-ui, sans-serif; letter-spacing: -0.01em; min-height: 100vh;">
|
||||
|
||||
<div hidden="{{ appOff }}" style="display: grid; grid-template-rows: 1fr auto; min-height: 100vh;">
|
||||
<div style="display: flex; align-items: center; justify-content: center; padding: 24px;">
|
||||
<div style="width: 100%; max-width: 420px; padding: 36px; border-radius: 24px; background: var(--surface); border: 1px solid var(--border); box-shadow: var(--shadow);">
|
||||
<div style="display: flex; align-items: center; gap: 10px; margin-bottom: 26px;">
|
||||
<span style="position: relative; width: 26px; height: 26px; display: block; flex: none;">
|
||||
<span style="position: absolute; left: 0; top: 3px; width: 19px; height: 19px; border-radius: 6px; background: linear-gradient(140deg, var(--accent), color-mix(in oklab, var(--accent) 55%, var(--glow-3))); display: block;"></span>
|
||||
<span style="position: absolute; right: 0; bottom: 3px; width: 19px; height: 19px; border-radius: 6px; background: var(--veil); border: 1px solid color-mix(in oklab, var(--accent) 35%, transparent); display: block;"></span>
|
||||
</span>
|
||||
<span style="font-family: 'Unbounded', 'Commissioner', sans-serif; font-weight: 600; font-size: 16px; letter-spacing: -0.02em;">Вейликс</span>
|
||||
</div>
|
||||
<h1 style="margin: 0 0 10px; font-size: 26px; font-weight: 600; letter-spacing: -0.03em;">Вход в кабинет</h1>
|
||||
<p style="margin: 0 0 22px; font-size: 15px; line-height: 1.55; color: var(--ink-2); font-weight: 300;">Введите почту, пришлём одноразовый код. Отдельной регистрации нет — кабинет создастся сам, если вы здесь впервые.</p>
|
||||
<div hidden="{{ otpSent }}" style="display: grid; gap: 10px;">
|
||||
<input type="email" placeholder="you@example.com" style="height: 50px; padding: 0 14px; border-radius: 12px; border: 1px solid var(--border); background: var(--bg); color: var(--ink); font-family: inherit; font-size: 15px; width: 100%;" />
|
||||
<button type="button" onClick="{{ sendOtp }}" style="height: 50px; border-radius: 12px; border: 0; background: var(--accent); color: var(--on-accent); font-family: inherit; font-size: 16px; font-weight: 500; cursor: pointer;" style-hover="background: var(--accent-hover);">Прислать код</button>
|
||||
</div>
|
||||
<div hidden="{{ otpNotSent }}" style="display: grid; gap: 10px;">
|
||||
<input type="text" inputmode="numeric" placeholder="6 цифр" style="height: 50px; padding: 0 14px; border-radius: 12px; border: 1px solid var(--border); background: var(--bg); color: var(--ink); font-family: inherit; font-size: 17px; letter-spacing: 0.28em; font-variant-numeric: tabular-nums; width: 100%;" />
|
||||
<button type="button" onClick="{{ signIn }}" style="height: 50px; border-radius: 12px; border: 0; background: var(--accent); color: var(--on-accent); font-family: inherit; font-size: 16px; font-weight: 500; cursor: pointer;" style-hover="background: var(--accent-hover);">Войти</button>
|
||||
<button type="button" onClick="{{ backToEmail }}" style="justify-self: start; border: 0; background: transparent; color: var(--ink-3); font-family: inherit; font-size: 14px; cursor: pointer; padding: 4px 0;">Другая почта</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer style="display: flex; flex-wrap: wrap; align-items: center; gap: 12px 20px; padding: 14px 24px; border-top: 1px solid var(--border); font-size: 13px; color: var(--ink-3);">
|
||||
<span>© 2026 Вейликс</span>
|
||||
<a href="#legal" style="color: var(--ink-3);" style-hover="color: var(--ink);">Условия</a>
|
||||
<a href="#legal" style="color: var(--ink-3);" style-hover="color: var(--ink);">Конфиденциальность</a>
|
||||
<a href="Support.dc.html" style="color: var(--ink-3);" style-hover="color: var(--ink);">Поддержка</a>
|
||||
<div style="margin-left: auto; display: flex; align-items: center; gap: 8px;">
|
||||
<div style="display: flex; width: max-content; padding: 3px; border-radius: 10px; border: 1px solid var(--border); background: var(--surface);">
|
||||
<button type="button" onClick="{{ setRu }}" style="padding: 6px 11px; border: 0; border-radius: 7px; background: {{ ruBg }}; color: {{ ruInk }}; font-family: inherit; font-size: 12px; cursor: pointer;">RU</button>
|
||||
<button type="button" onClick="{{ setEn }}" style="padding: 6px 11px; border: 0; border-radius: 7px; background: {{ enBg }}; color: {{ enInk }}; font-family: inherit; font-size: 12px; cursor: pointer;">EN</button>
|
||||
</div>
|
||||
<div style="display: flex; width: max-content; padding: 3px; border-radius: 10px; border: 1px solid var(--border); background: var(--surface);">
|
||||
<button type="button" onClick="{{ setLight }}" title="Светлая тема" style="display: flex; align-items: center; justify-content: center; width: 32px; height: 32px; border: 0; border-radius: 7px; background: {{ lightBg }}; color: {{ lightInk }}; cursor: pointer;">
|
||||
<span style="position: relative; width: 16px; height: 16px; display: block;">
|
||||
<span style="position: absolute; left: 3.5px; top: 3.5px; width: 9px; height: 9px; border-radius: 50%; background: currentColor; display: block;"></span>
|
||||
<span style="position: absolute; left: 0; top: 0; width: 16px; height: 16px; border-radius: 50%; border: 1.5px solid currentColor; opacity: 0.45; display: block;"></span>
|
||||
</span>
|
||||
</button>
|
||||
<button type="button" onClick="{{ setDark }}" title="Тёмная тема" style="display: flex; align-items: center; justify-content: center; width: 32px; height: 32px; border: 0; border-radius: 7px; background: {{ darkBg }}; color: {{ darkInk }}; cursor: pointer;">
|
||||
<span style="width: 16px; height: 16px; border-radius: 50%; box-shadow: inset -5px -2px 0 0 currentColor; display: block;"></span>
|
||||
</button>
|
||||
<button type="button" onClick="{{ setSystem }}" title="Как в системе" style="display: flex; align-items: center; justify-content: center; width: 32px; height: 32px; border: 0; border-radius: 7px; background: {{ systemBg }}; color: {{ systemInk }}; cursor: pointer;">
|
||||
<span style="width: 16px; height: 16px; border-radius: 50%; border: 1.5px solid currentColor; background: linear-gradient(90deg, currentColor 50%, transparent 50%); display: block;"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<div hidden="{{ loginOff }}" style="display: grid; grid-template-columns: {{ shellCols }}; min-height: 100vh;">
|
||||
|
||||
<div hidden="{{ topbarOff }}" style="position: sticky; top: 0; z-index: 30; display: flex; align-items: center; gap: 10px; padding: 12px 16px; background: color-mix(in oklab, var(--bg) 85%, transparent); backdrop-filter: blur(18px); -webkit-backdrop-filter: blur(18px); border-bottom: 1px solid var(--border);">
|
||||
<a href="Main.dc.html" style="display: flex; align-items: center; gap: 10px; color: var(--ink);">
|
||||
<span style="position: relative; width: 24px; height: 24px; display: block; flex: none;">
|
||||
<span style="position: absolute; left: 0; top: 3px; width: 18px; height: 18px; border-radius: 6px; background: linear-gradient(140deg, var(--accent), color-mix(in oklab, var(--accent) 55%, var(--glow-3))); display: block;"></span>
|
||||
<span style="position: absolute; right: 0; bottom: 3px; width: 18px; height: 18px; border-radius: 6px; background: var(--veil); border: 1px solid color-mix(in oklab, var(--accent) 35%, transparent); display: block;"></span>
|
||||
</span>
|
||||
<span style="font-family: 'Unbounded', 'Commissioner', sans-serif; font-weight: 600; font-size: 15px; letter-spacing: -0.02em;">Вейликс</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<aside hidden="{{ asideOff }}" style="display: flex; flex-direction: column; gap: 24px; padding: 22px 16px; border-right: 1px solid var(--border); background: var(--surface-2); position: sticky; top: 0; height: 100vh;">
|
||||
<a href="Main.dc.html" style="display: flex; align-items: center; gap: 10px; padding: 4px 8px; color: var(--ink);">
|
||||
<span style="position: relative; width: 24px; height: 24px; display: block; flex: none;">
|
||||
<span style="position: absolute; left: 0; top: 3px; width: 18px; height: 18px; border-radius: 6px; background: linear-gradient(140deg, var(--accent), color-mix(in oklab, var(--accent) 55%, var(--glow-3))); display: block;"></span>
|
||||
<span style="position: absolute; right: 0; bottom: 3px; width: 18px; height: 18px; border-radius: 6px; background: var(--veil); border: 1px solid color-mix(in oklab, var(--accent) 35%, transparent); display: block;"></span>
|
||||
</span>
|
||||
<span style="font-family: 'Unbounded', 'Commissioner', sans-serif; font-weight: 600; font-size: 15px; letter-spacing: -0.02em;">Вейликс</span>
|
||||
</a>
|
||||
|
||||
<nav style="display: grid; gap: 4px;">
|
||||
<button type="button" onClick="{{ goHome }}" style="display: flex; align-items: center; gap: 12px; padding: 11px 12px; border-radius: 12px; border: 0; background: {{ homeBg }}; color: {{ homeInk }}; font-family: inherit; font-size: 15px; text-align: left; cursor: pointer;" style-hover="background: var(--surface);">
|
||||
<span style="position: relative; width: 18px; height: 18px; flex: none; display: block;">
|
||||
<span style="position: absolute; left: 0; top: 0; width: 8px; height: 8px; border-radius: 2px; background: currentColor; display: block;"></span>
|
||||
<span style="position: absolute; right: 0; top: 0; width: 8px; height: 8px; border-radius: 2px; background: currentColor; opacity: 0.45; display: block;"></span>
|
||||
<span style="position: absolute; left: 0; bottom: 0; width: 8px; height: 8px; border-radius: 2px; background: currentColor; opacity: 0.45; display: block;"></span>
|
||||
<span style="position: absolute; right: 0; bottom: 0; width: 8px; height: 8px; border-radius: 2px; background: currentColor; display: block;"></span>
|
||||
</span>
|
||||
Главная
|
||||
</button>
|
||||
<button type="button" onClick="{{ goKeys }}" style="display: flex; align-items: center; gap: 12px; padding: 11px 12px; border-radius: 12px; border: 0; background: {{ keysBg }}; color: {{ keysInk }}; font-family: inherit; font-size: 15px; text-align: left; cursor: pointer;" style-hover="background: var(--surface);">
|
||||
<span style="position: relative; width: 18px; height: 18px; flex: none; display: block;">
|
||||
<span style="position: absolute; left: 0; top: 2px; width: 18px; height: 4px; border-radius: 2px; background: currentColor; display: block;"></span>
|
||||
<span style="position: absolute; left: 0; top: 8px; width: 18px; height: 4px; border-radius: 2px; background: currentColor; opacity: 0.6; display: block;"></span>
|
||||
<span style="position: absolute; left: 0; top: 14px; width: 18px; height: 4px; border-radius: 2px; background: currentColor; opacity: 0.3; display: block;"></span>
|
||||
</span>
|
||||
Список ключей
|
||||
</button>
|
||||
<button type="button" onClick="{{ goBuy }}" style="display: flex; align-items: center; gap: 12px; padding: 11px 12px; border-radius: 12px; border: 0; background: {{ buyBg }}; color: {{ buyInk }}; font-family: inherit; font-size: 15px; text-align: left; cursor: pointer;" style-hover="background: var(--surface);">
|
||||
<span style="position: relative; width: 18px; height: 18px; flex: none; display: block;">
|
||||
<span style="position: absolute; left: 7px; top: 0; width: 4px; height: 18px; border-radius: 2px; background: currentColor; display: block;"></span>
|
||||
<span style="position: absolute; left: 0; top: 7px; width: 18px; height: 4px; border-radius: 2px; background: currentColor; display: block;"></span>
|
||||
</span>
|
||||
Купить ключ
|
||||
</button>
|
||||
<button type="button" onClick="{{ goBilling }}" style="display: flex; align-items: center; gap: 12px; padding: 11px 12px; border-radius: 12px; border: 0; background: {{ billingBg }}; color: {{ billingInk }}; font-family: inherit; font-size: 15px; text-align: left; cursor: pointer;" style-hover="background: var(--surface);">
|
||||
<span style="position: relative; width: 18px; height: 18px; flex: none; display: block;">
|
||||
<span style="position: absolute; left: 0; top: 2px; width: 18px; height: 14px; border-radius: 4px; border: 2px solid currentColor; display: block;"></span>
|
||||
<span style="position: absolute; left: 0; top: 6px; width: 18px; height: 3px; background: currentColor; display: block;"></span>
|
||||
</span>
|
||||
Счёт и платежи
|
||||
</button>
|
||||
<button type="button" onClick="{{ goRef }}" style="display: flex; align-items: center; gap: 12px; padding: 11px 12px; border-radius: 12px; border: 0; background: {{ refBg }}; color: {{ refInk }}; font-family: inherit; font-size: 15px; text-align: left; cursor: pointer;" style-hover="background: var(--surface);">
|
||||
<span style="position: relative; width: 18px; height: 18px; flex: none; display: block;">
|
||||
<span style="position: absolute; left: 0; top: 5px; width: 8px; height: 8px; border-radius: 50%; border: 2px solid currentColor; display: block;"></span>
|
||||
<span style="position: absolute; right: 0; top: 0; width: 8px; height: 8px; border-radius: 50%; background: currentColor; display: block;"></span>
|
||||
<span style="position: absolute; right: 2px; bottom: 0; width: 8px; height: 8px; border-radius: 50%; background: currentColor; opacity: 0.45; display: block;"></span>
|
||||
</span>
|
||||
Реферальная программа
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
<div style="margin-top: auto; display: grid; gap: 14px; padding: 0 4px;">
|
||||
<a href="Support.dc.html" style="display: flex; align-items: center; gap: 12px; padding: 11px 8px; border-radius: 12px; color: var(--ink-2); font-size: 15px;" style-hover="background: var(--surface); color: var(--ink);">
|
||||
<span style="position: relative; width: 18px; height: 18px; flex: none; display: block;">
|
||||
<span style="position: absolute; left: 1px; top: 1px; width: 16px; height: 16px; border-radius: 8px 8px 0 0; border: 2px solid currentColor; border-bottom: 0; display: block;"></span>
|
||||
<span style="position: absolute; left: 0; top: 8px; width: 5px; height: 9px; border-radius: 2px; background: currentColor; display: block;"></span>
|
||||
<span style="position: absolute; right: 0; top: 8px; width: 5px; height: 9px; border-radius: 2px; background: currentColor; display: block;"></span>
|
||||
</span>
|
||||
Поддержка
|
||||
</a>
|
||||
<div>
|
||||
<div style="font-size: 12px; color: var(--ink-3); margin-bottom: 6px; padding: 0 4px;">Тема</div>
|
||||
<div style="display: flex; flex-wrap: wrap; align-items: center; gap: 6px; width: max-content; max-width: 100%; align-self: start; padding: 3px; border-radius: 12px; border: 1px solid var(--border); background: var(--surface);">
|
||||
<button type="button" onClick="{{ setLight }}" title="Светлая тема" style="display: flex; align-items: center; justify-content: center; width: 36px; height: 36px; border: 0; border-radius: 9px; background: {{ lightBg }}; color: {{ lightInk }}; cursor: pointer;">
|
||||
<span style="position: relative; width: 18px; height: 18px; display: block;">
|
||||
<span style="position: absolute; left: 4px; top: 4px; width: 10px; height: 10px; border-radius: 50%; background: currentColor; display: block;"></span>
|
||||
<span style="position: absolute; left: 0; top: 0; width: 18px; height: 18px; border-radius: 50%; border: 1.5px solid currentColor; opacity: 0.45; display: block;"></span>
|
||||
</span>
|
||||
</button>
|
||||
<button type="button" onClick="{{ setDark }}" title="Тёмная тема" style="display: flex; align-items: center; justify-content: center; width: 36px; height: 36px; border: 0; border-radius: 9px; background: {{ darkBg }}; color: {{ darkInk }}; cursor: pointer;">
|
||||
<span style="width: 18px; height: 18px; border-radius: 50%; box-shadow: inset -6px -2px 0 0 currentColor; display: block;"></span>
|
||||
</button>
|
||||
<button type="button" onClick="{{ setSystem }}" title="Как в системе" style="display: flex; align-items: center; justify-content: center; width: 36px; height: 36px; border: 0; border-radius: 9px; background: {{ systemBg }}; color: {{ systemInk }}; cursor: pointer;">
|
||||
<span style="width: 18px; height: 18px; border-radius: 50%; border: 1.5px solid currentColor; background: linear-gradient(90deg, currentColor 50%, transparent 50%); display: block;"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size: 12px; color: var(--ink-3); margin-bottom: 6px; padding: 0 4px;">Язык</div>
|
||||
<div style="display: flex; flex-wrap: wrap; width: max-content; max-width: 100%; align-self: start; padding: 3px; border-radius: 12px; border: 1px solid var(--border); background: var(--surface);">
|
||||
<button type="button" onClick="{{ setRu }}" style="padding: 8px 14px; border: 0; border-radius: 9px; background: {{ ruBg }}; color: {{ ruInk }}; font-family: inherit; font-size: 13px; cursor: pointer;">RU</button>
|
||||
<button type="button" onClick="{{ setEn }}" style="padding: 8px 14px; border: 0; border-radius: 9px; background: {{ enBg }}; color: {{ enInk }}; font-family: inherit; font-size: 13px; cursor: pointer;">EN</button>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" onClick="{{ signOut }}" style="justify-self: start; border: 0; background: transparent; color: var(--ink-3); font-family: inherit; font-size: 13px; cursor: pointer; padding: 4px 8px;">Выйти · anna@example.com</button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<main style="padding: {{ mainPad }}; max-width: 960px;">
|
||||
|
||||
<div hidden="{{ homeOff }}" style="display: grid; gap: 28px;">
|
||||
<div>
|
||||
<h1 style="margin: 0 0 6px; font-size: 30px; font-weight: 600; letter-spacing: -0.03em;">Ваши ключи</h1>
|
||||
<p style="margin: 0; font-size: 15px; color: var(--ink-2); font-weight: 300;">Два активных тарифа, каждый со своим индивидуальным IP.</p>
|
||||
</div>
|
||||
|
||||
<div style="display: grid; gap: 12px;">
|
||||
<sc-for list="{{ keys }}" as="key" hint-placeholder-count="2">
|
||||
<button type="button" onClick="{{ key.open }}" style="display: grid; gap: 16px; padding: 24px; border-radius: 20px; border: 1px solid var(--border); background: var(--surface); box-shadow: var(--shadow); color: var(--ink); font-family: inherit; text-align: left; cursor: pointer; width: 100%;" style-hover="border-color: var(--accent);">
|
||||
<span style="display: flex; flex-wrap: wrap; align-items: center; gap: 12px;">
|
||||
<span style="font-size: 19px; font-weight: 500; letter-spacing: -0.02em;">{{ key.title }}</span>
|
||||
<span style="padding: 4px 10px; border-radius: 8px; background: {{ key.badgeBg }}; color: {{ key.badgeInk }}; font-size: 12px;">{{ key.badge }}</span>
|
||||
<span style="margin-left: auto; font-size: 14px; color: var(--ink-3); font-variant-numeric: tabular-nums;">{{ key.ip }}</span>
|
||||
</span>
|
||||
<span style="display: flex; flex-wrap: wrap; gap: 8px 28px; font-size: 14px; color: var(--ink-2);">
|
||||
<span>Действует до {{ key.until }}</span>
|
||||
<span>{{ key.devices }}</span>
|
||||
<span>{{ key.payment }}</span>
|
||||
</span>
|
||||
</button>
|
||||
</sc-for>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 16px; padding: 24px; border-radius: 20px; background: var(--surface-2); border: 1px dashed var(--border);">
|
||||
<div style="display: grid; gap: 4px;">
|
||||
<div style="font-size: 17px; font-weight: 500;">Нужен ещё один IP или другая страна</div>
|
||||
<div style="font-size: 14px; color: var(--ink-2); font-weight: 300;">Новый ключ появится здесь же, оплата отдельная.</div>
|
||||
</div>
|
||||
<button type="button" onClick="{{ goBuy }}" style="height: 46px; padding: 0 22px; border-radius: 12px; border: 0; background: var(--accent); color: var(--on-accent); font-family: inherit; font-size: 15px; font-weight: 500; cursor: pointer;" style-hover="background: var(--accent-hover);">Купить ключ</button>
|
||||
</div>
|
||||
|
||||
<div style="display: grid; gap: 10px;">
|
||||
<h2 style="margin: 0 0 4px; font-size: 20px; font-weight: 600; letter-spacing: -0.025em;">Короткие ответы</h2>
|
||||
<sc-for list="{{ faq }}" as="item" hint-placeholder-count="3">
|
||||
<div style="border: 1px solid var(--border); border-radius: 16px; background: var(--surface); overflow: hidden;">
|
||||
<button type="button" onClick="{{ item.toggle }}" style="display: flex; align-items: center; gap: 16px; width: 100%; padding: 17px 20px; background: transparent; border: 0; color: var(--ink); font-family: inherit; font-size: 16px; font-weight: 500; text-align: left; cursor: pointer;">
|
||||
<span style="flex: 1;">{{ item.q }}</span>
|
||||
<span style="flex: none; width: 9px; height: 9px; border-right: 1.5px solid var(--ink-3); border-bottom: 1.5px solid var(--ink-3); transform: {{ item.rot }}; transition: transform 180ms ease; display: block;"></span>
|
||||
</button>
|
||||
<div hidden="{{ item.off }}" style="padding: 0 20px 20px; font-size: 15px; line-height: 1.6; color: var(--ink-2); font-weight: 300; max-width: 62ch;">{{ item.a }}</div>
|
||||
</div>
|
||||
</sc-for>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div hidden="{{ keysOff }}" style="display: grid; gap: 20px;">
|
||||
<h1 style="margin: 0; font-size: 30px; font-weight: 600; letter-spacing: -0.03em;">Список ключей</h1>
|
||||
<div style="display: grid; gap: 12px;">
|
||||
<sc-for list="{{ allKeys }}" as="key" hint-placeholder-count="3">
|
||||
<button type="button" onClick="{{ key.open }}" style="display: flex; flex-wrap: wrap; align-items: center; gap: 12px 20px; padding: 20px 24px; border-radius: 18px; border: 1px solid var(--border); background: var(--surface); color: var(--ink); font-family: inherit; text-align: left; cursor: pointer; width: 100%;" style-hover="border-color: var(--accent);">
|
||||
<span style="font-size: 17px; font-weight: 500;">{{ key.title }}</span>
|
||||
<span style="padding: 4px 10px; border-radius: 8px; background: {{ key.badgeBg }}; color: {{ key.badgeInk }}; font-size: 12px;">{{ key.badge }}</span>
|
||||
<span style="margin-left: auto; font-size: 14px; color: var(--ink-3); font-variant-numeric: tabular-nums;">{{ key.ip }}</span>
|
||||
<span style="font-size: 14px; color: var(--ink-2); font-variant-numeric: tabular-nums;">{{ key.untilLine }}</span>
|
||||
</button>
|
||||
</sc-for>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div hidden="{{ keyOff }}" style="display: grid; gap: 22px;">
|
||||
<button type="button" onClick="{{ goKeys }}" style="justify-self: start; border: 0; background: transparent; color: var(--ink-3); font-family: inherit; font-size: 14px; cursor: pointer; padding: 0;">← Все ключи</button>
|
||||
|
||||
<div style="display: flex; flex-wrap: wrap; align-items: baseline; gap: 12px 20px;">
|
||||
<h1 style="margin: 0; font-size: 30px; font-weight: 600; letter-spacing: -0.03em;">{{ cur.title }}</h1>
|
||||
<span style="font-size: 15px; color: var(--ink-3); font-variant-numeric: tabular-nums;">{{ cur.ip }}</span>
|
||||
</div>
|
||||
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 12px;">
|
||||
<div style="padding: 22px; border-radius: 18px; background: var(--surface); border: 1px solid var(--border); box-shadow: var(--shadow);">
|
||||
<div style="font-size: 13px; color: var(--ink-3); margin-bottom: 10px;">Действует до</div>
|
||||
<div style="font-size: 24px; font-weight: 600; letter-spacing: -0.025em;">{{ cur.until }}</div>
|
||||
<div style="margin-top: 8px; font-size: 14px; color: {{ cur.leftInk }};">{{ cur.left }}</div>
|
||||
</div>
|
||||
<div style="padding: 22px; border-radius: 18px; background: var(--surface); border: 1px solid var(--border); box-shadow: var(--shadow);">
|
||||
<div style="font-size: 13px; color: var(--ink-3); margin-bottom: 10px;">Оплата</div>
|
||||
<div style="font-size: 18px; font-variant-numeric: tabular-nums;">{{ cur.card }}</div>
|
||||
<div style="margin-top: 8px; font-size: 14px; color: var(--ink-2);">2 499 ₽ в месяц · карта аккаунта</div>
|
||||
<button type="button" onClick="{{ goBilling }}" style="margin-top: 14px; height: 40px; padding: 0 16px; border-radius: 10px; border: 1px solid var(--border); background: transparent; color: var(--ink); font-family: inherit; font-size: 14px; cursor: pointer;" style-hover="border-color: var(--ink-3);">Счёт и платежи</button>
|
||||
</div>
|
||||
<div style="padding: 22px; border-radius: 18px; background: var(--surface); border: 1px solid var(--border); box-shadow: var(--shadow);">
|
||||
<div style="font-size: 13px; color: var(--ink-3); margin-bottom: 10px;">Устройства</div>
|
||||
<div style="font-size: 24px; font-weight: 600; letter-spacing: -0.025em;">{{ cur.devices }}</div>
|
||||
<div style="margin-top: 8px; font-size: 14px; color: var(--ink-2);">Один и тот же IP на всех</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="padding: 26px; border-radius: 20px; background: var(--surface); border: 1px solid var(--border); box-shadow: var(--shadow);">
|
||||
<div style="display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; gap: 12px; margin-bottom: 6px;">
|
||||
<h2 style="margin: 0; font-size: 20px; font-weight: 600; letter-spacing: -0.025em;">WireGuard</h2>
|
||||
<span style="font-size: 13px; color: var(--ink-3);">5 конфигураций — по одной на устройство</span>
|
||||
</div>
|
||||
<p style="margin: 0 0 18px; font-size: 14px; color: var(--ink-2); font-weight: 300; max-width: 60ch;">На компьютере скачайте файл и откройте его в приложении. На телефоне — отсканируйте QR-код.</p>
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(210px, 1fr)); gap: 10px;">
|
||||
<sc-for list="{{ configs }}" as="cfg" hint-placeholder-count="5">
|
||||
<div style="display: grid; gap: 12px; padding: 16px; border-radius: 14px; background: var(--surface-2); border: 1px solid var(--border);">
|
||||
<div style="display: flex; align-items: center; justify-content: space-between; gap: 10px;">
|
||||
<span style="font-size: 15px; font-weight: 500;">{{ cfg.name }}</span>
|
||||
<span style="font-size: 12px; color: var(--ink-3);">{{ cfg.state }}</span>
|
||||
</div>
|
||||
<div style="display: flex; gap: 8px;">
|
||||
<button type="button" style="flex: 1; height: 38px; border-radius: 10px; border: 0; background: var(--accent); color: var(--on-accent); font-family: inherit; font-size: 14px; cursor: pointer;" style-hover="background: var(--accent-hover);">Скачать</button>
|
||||
<button type="button" onClick="{{ cfg.showQr }}" style="height: 38px; padding: 0 14px; border-radius: 10px; border: 1px solid var(--border); background: var(--bg); color: var(--ink); font-family: inherit; font-size: 14px; cursor: pointer;" style-hover="border-color: var(--ink-3);">QR</button>
|
||||
</div>
|
||||
</div>
|
||||
</sc-for>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="padding: 26px; border-radius: 20px; background: var(--surface); border: 1px solid var(--border); box-shadow: var(--shadow);">
|
||||
<div style="display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; gap: 12px; margin-bottom: 6px;">
|
||||
<h2 style="margin: 0; font-size: 20px; font-weight: 600; letter-spacing: -0.025em;">Xray</h2>
|
||||
<span style="font-size: 13px; color: var(--ink-3);">Для Happ и других клиентов</span>
|
||||
</div>
|
||||
<p style="margin: 0 0 18px; font-size: 14px; color: var(--ink-2); font-weight: 300; max-width: 60ch;">Запасной способ подключения. Скопируйте ссылку или отсканируйте код — настройки те же.</p>
|
||||
<div style="display: flex; flex-wrap: wrap; gap: 10px; align-items: center;">
|
||||
<div style="flex: 1; min-width: 240px; padding: 14px 16px; border-radius: 12px; background: var(--surface-2); border: 1px solid var(--border); font-size: 13px; color: var(--ink-2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-variant-numeric: tabular-nums;">{{ cur.xray }}</div>
|
||||
<button type="button" onClick="{{ copyXray }}" style="height: 46px; padding: 0 20px; border-radius: 12px; border: 0; background: var(--accent); color: var(--on-accent); font-family: inherit; font-size: 15px; cursor: pointer;" style-hover="background: var(--accent-hover);">{{ copyLabel }}</button>
|
||||
<button type="button" onClick="{{ showXrayQr }}" style="height: 46px; padding: 0 18px; border-radius: 12px; border: 1px solid var(--border); background: transparent; color: var(--ink); font-family: inherit; font-size: 15px; cursor: pointer;" style-hover="border-color: var(--ink-3);">QR</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div hidden="{{ buyOff }}" style="display: grid; gap: 22px;">
|
||||
<div>
|
||||
<h1 style="margin: 0 0 6px; font-size: 30px; font-weight: 600; letter-spacing: -0.03em;">Купить ключ</h1>
|
||||
<p style="margin: 0; font-size: 15px; color: var(--ink-2); font-weight: 300;">Базовая цена: 4 900 ₽ первый месяц, далее 2 499 ₽. В редких локациях — выше.</p>
|
||||
</div>
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 10px;">
|
||||
<sc-for list="{{ locations }}" as="loc" hint-placeholder-count="12">
|
||||
<div role="button" tabindex="0" onClick="{{ loc.pick }}" style="display: grid; align-content: start; gap: 14px; padding: 20px; border-radius: 18px; border: 1px solid {{ loc.border }}; background: {{ loc.bg }}; color: var(--ink); cursor: pointer;" style-hover="border-color: var(--ink-3);">
|
||||
<span style="display: flex; align-items: center; justify-content: space-between; gap: 14px;">
|
||||
{{ loc.flag }}
|
||||
<span style="display: flex; align-items: center; gap: 12px; flex: none; min-height: 32px;">
|
||||
<span hidden="{{ loc.buyOff }}" style="display: flex; align-items: center; justify-content: center; height: 32px; padding: 0 16px; border-radius: 10px; background: var(--accent); color: var(--on-accent); font-size: 14px; font-weight: 500; cursor: pointer;">Купить</span>
|
||||
<span hidden="{{ loc.dotOff }}" style="width: 11px; height: 11px; border-radius: 50%; background: {{ loc.dot }}; display: block; flex: none;"></span>
|
||||
</span>
|
||||
</span>
|
||||
<span style="display: grid; gap: 5px;">
|
||||
<span style="font-size: 17px; font-weight: 500; letter-spacing: -0.02em;">{{ loc.country }}</span>
|
||||
<span style="font-size: 14px; color: var(--ink-3);">{{ loc.city }}</span>
|
||||
</span>
|
||||
<span style="font-size: 13px; color: {{ loc.statusInk }};">{{ loc.status }}</span>
|
||||
<span style="display: grid; gap: 12px; padding-top: 14px; border-top: 1px solid var(--border);">
|
||||
<span style="display: flex; flex-wrap: wrap; align-items: baseline; gap: 6px; font-size: 13px; color: var(--ink-2); font-variant-numeric: tabular-nums;">
|
||||
<span style="font-size: 17px; font-weight: 500; color: var(--ink);">{{ loc.setup }}</span>
|
||||
первый месяц · далее
|
||||
<span style="font-weight: 500; color: var(--ink);">{{ loc.month }}</span>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</sc-for>
|
||||
</div>
|
||||
<div style="padding: 24px; border-radius: 18px; background: var(--surface); border: 1px solid var(--border);">
|
||||
<h2 id="lead" style="margin: 0 0 8px; font-size: 19px; font-weight: 600; letter-spacing: -0.02em; scroll-margin-top: 24px;">Нужны индивидуальные условия</h2>
|
||||
<p style="margin: 0 0 18px; max-width: 60ch; font-size: 14px; line-height: 1.55; color: var(--ink-2); font-weight: 300;">Команда, много адресов, редкая страна или свой сценарий маршрутизации. Оставьте заявку — ответим в течение дня.</p>
|
||||
<button type="button" onClick="{{ openLead }}" hidden="{{ leadOpen }}" style="display: inline-flex; align-items: center; height: 46px; padding: 0 20px; border-radius: 12px; border: 1px solid var(--accent); background: transparent; color: var(--accent); font-family: inherit; font-size: 15px; font-weight: 500; cursor: pointer;" style-hover="background: var(--accent-soft);">Оставить заявку</button>
|
||||
<div hidden="{{ leadClosed }}" style="display: grid; gap: 10px; max-width: 460px;">
|
||||
<input type="text" placeholder="Имя" style="height: 48px; padding: 0 14px; border-radius: 12px; border: 1px solid var(--border); background: var(--bg); color: var(--ink); font-family: inherit; font-size: 15px; width: 100%;" />
|
||||
<input type="email" placeholder="Email" style="height: 48px; padding: 0 14px; border-radius: 12px; border: 1px solid var(--border); background: var(--bg); color: var(--ink); font-family: inherit; font-size: 15px; width: 100%;" />
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 10px;">
|
||||
<input type="text" placeholder="Telegram" style="height: 48px; padding: 0 14px; border-radius: 12px; border: 1px solid var(--border); background: var(--bg); color: var(--ink); font-family: inherit; font-size: 15px; width: 100%;" />
|
||||
<input type="text" placeholder="WhatsApp" style="height: 48px; padding: 0 14px; border-radius: 12px; border: 1px solid var(--border); background: var(--bg); color: var(--ink); font-family: inherit; font-size: 15px; width: 100%;" />
|
||||
</div>
|
||||
<textarea rows="3" placeholder="Коротко о задаче" style="padding: 12px 14px; border-radius: 12px; border: 1px solid var(--border); background: var(--bg); color: var(--ink); font-family: inherit; font-size: 15px; resize: vertical; width: 100%;"></textarea>
|
||||
<button type="button" onClick="{{ sendLead }}" style="display: inline-flex; align-items: center; justify-content: center; height: 48px; padding: 0 22px; border-radius: 12px; border: 0; background: var(--accent); color: var(--on-accent); font-family: inherit; font-size: 15px; font-weight: 500; cursor: pointer;" style-hover="background: var(--accent-hover);">{{ leadButton }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div hidden="{{ billingOff }}" style="display: grid; gap: 22px;">
|
||||
<div>
|
||||
<h1 style="margin: 0 0 6px; font-size: 30px; font-weight: 600; letter-spacing: -0.03em;">Счёт и платежи</h1>
|
||||
<p style="margin: 0; max-width: 62ch; font-size: 15px; line-height: 1.55; color: var(--ink-2); font-weight: 300;">Карта привязана к аккаунту, а не к отдельному ключу. Списания за все ключи идут с неё — или с баланса, если на нём есть деньги.</p>
|
||||
</div>
|
||||
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); gap: 12px;">
|
||||
<div style="display: grid; align-content: start; gap: 8px; padding: 22px; border-radius: 18px; background: var(--surface); border: 1px solid var(--border); box-shadow: var(--shadow);">
|
||||
<div style="font-size: 13px; color: var(--ink-3);">Баланс</div>
|
||||
<div style="font-size: 30px; font-weight: 600; letter-spacing: -0.03em; font-variant-numeric: tabular-nums;">1 250 ₽</div>
|
||||
<div style="font-size: 14px; color: var(--ink-2);">Списывается в первую очередь</div>
|
||||
<button type="button" onClick="{{ openTopup }}" style="justify-self: start; margin-top: 6px; height: 40px; padding: 0 16px; border-radius: 10px; border: 0; background: var(--accent); color: var(--on-accent); font-family: inherit; font-size: 14px; font-weight: 500; cursor: pointer;" style-hover="background: var(--accent-hover);">Пополнить</button>
|
||||
</div>
|
||||
<div style="display: grid; align-content: start; gap: 8px; padding: 22px; border-radius: 18px; background: var(--surface); border: 1px solid var(--border); box-shadow: var(--shadow);">
|
||||
<div style="font-size: 13px; color: var(--ink-3);">Способ оплаты</div>
|
||||
<div style="font-size: 20px; font-weight: 500; font-variant-numeric: tabular-nums;">Карта ···· 4242</div>
|
||||
<div style="font-size: 14px; color: var(--ink-2);">Основная для всех ключей аккаунта</div>
|
||||
<button type="button" style="justify-self: start; margin-top: 6px; height: 40px; padding: 0 16px; border-radius: 10px; border: 1px solid var(--border); background: transparent; color: var(--ink); font-family: inherit; font-size: 14px; cursor: pointer;" style-hover="border-color: var(--ink-3);">Изменить карту</button>
|
||||
</div>
|
||||
<div style="display: grid; align-content: start; gap: 8px; padding: 22px; border-radius: 18px; background: var(--surface); border: 1px solid var(--border); box-shadow: var(--shadow);">
|
||||
<div style="font-size: 13px; color: var(--ink-3);">Следующее списание</div>
|
||||
<div style="font-size: 30px; font-weight: 600; letter-spacing: -0.03em; font-variant-numeric: tabular-nums;">2 499 ₽</div>
|
||||
<div style="font-size: 14px; color: var(--ink-2);">21 сентября · США · Нью-Йорк</div>
|
||||
<div style="font-size: 13px; color: var(--ink-3);">Далее 14 октября · Нидерланды</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div hidden="{{ topupOff }}" style="display: grid; gap: 16px; padding: 26px; border-radius: 20px; background: var(--surface); border: 1px solid var(--border); box-shadow: var(--shadow);">
|
||||
<div style="display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; gap: 12px;">
|
||||
<h2 style="margin: 0; font-size: 20px; font-weight: 600; letter-spacing: -0.025em;">Пополнить баланс</h2>
|
||||
<button type="button" onClick="{{ closeTopup }}" style="border: 0; background: transparent; color: var(--ink-3); font-family: inherit; font-size: 14px; cursor: pointer; padding: 0;">Свернуть</button>
|
||||
</div>
|
||||
<div style="display: grid; gap: 8px;">
|
||||
<div style="font-size: 13px; color: var(--ink-3);">Сумма</div>
|
||||
<div style="display: flex; flex-wrap: wrap; gap: 8px;">
|
||||
<sc-for list="{{ sums }}" as="sum" hint-placeholder-count="4">
|
||||
<button type="button" onClick="{{ sum.pick }}" style="height: 44px; padding: 0 18px; border-radius: 12px; border: 1px solid {{ sum.border }}; background: {{ sum.bg }}; color: var(--ink); font-family: inherit; font-size: 15px; font-variant-numeric: tabular-nums; cursor: pointer;">{{ sum.label }}</button>
|
||||
</sc-for>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: grid; gap: 8px;">
|
||||
<div style="font-size: 13px; color: var(--ink-3);">Способ</div>
|
||||
<div style="display: flex; flex-wrap: wrap; gap: 8px;">
|
||||
<sc-for list="{{ methods }}" as="m" hint-placeholder-count="3">
|
||||
<button type="button" onClick="{{ m.pick }}" style="height: 44px; padding: 0 18px; border-radius: 12px; border: 1px solid {{ m.border }}; background: {{ m.bg }}; color: var(--ink); font-family: inherit; font-size: 15px; cursor: pointer;">{{ m.label }}</button>
|
||||
</sc-for>
|
||||
</div>
|
||||
</div>
|
||||
<div hidden="{{ cryptoOff }}" style="display: grid; gap: 10px; padding: 18px; border-radius: 14px; background: var(--surface-2); border: 1px solid var(--border);">
|
||||
<div style="font-size: 15px; font-weight: 500;">USDT (TRC-20), BTC, ETH</div>
|
||||
<p style="margin: 0; max-width: 60ch; font-size: 14px; line-height: 1.55; color: var(--ink-2); font-weight: 300;">Покажем адрес и сумму в выбранной монете. Курс фиксируется на 15 минут, баланс пополнится после одного подтверждения сети.</p>
|
||||
</div>
|
||||
<button type="button" onClick="{{ sendTopup }}" style="justify-self: start; height: 48px; padding: 0 22px; border-radius: 12px; border: 0; background: var(--accent); color: var(--on-accent); font-family: inherit; font-size: 15px; font-weight: 500; cursor: pointer;" style-hover="background: var(--accent-hover);">{{ topupButton }}</button>
|
||||
</div>
|
||||
|
||||
<div style="display: grid; gap: 10px;">
|
||||
<h2 style="margin: 0 0 4px; font-size: 20px; font-weight: 600; letter-spacing: -0.025em;">История платежей</h2>
|
||||
<sc-for list="{{ payments }}" as="p" hint-placeholder-count="5">
|
||||
<div style="display: flex; flex-wrap: wrap; align-items: center; gap: 10px 20px; padding: 18px 20px; border-radius: 16px; background: var(--surface); border: 1px solid var(--border);">
|
||||
<span style="width: 116px; flex: none; font-size: 14px; color: var(--ink-3); font-variant-numeric: tabular-nums;">{{ p.date }}</span>
|
||||
<span style="flex: 1; min-width: 180px; display: grid; gap: 3px;">
|
||||
<span style="font-size: 15px;">{{ p.title }}</span>
|
||||
<span style="font-size: 13px; color: var(--ink-3);">{{ p.method }}</span>
|
||||
</span>
|
||||
<span style="font-size: 13px; color: var(--ink-3);">{{ p.status }}</span>
|
||||
<span style="width: 104px; flex: none; text-align: right; font-size: 16px; font-weight: 500; color: {{ p.ink }}; font-variant-numeric: tabular-nums;">{{ p.amount }}</span>
|
||||
</div>
|
||||
</sc-for>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div hidden="{{ refOff }}" style="display: grid; gap: 20px;">
|
||||
<div>
|
||||
<h1 style="margin: 0 0 6px; font-size: 30px; font-weight: 600; letter-spacing: -0.03em;">Реферальная программа</h1>
|
||||
<p style="margin: 0; max-width: 62ch; font-size: 15px; line-height: 1.55; color: var(--ink-2); font-weight: 300;">Приведите друга — вы оба получите по месяцу в подарок.</p>
|
||||
</div>
|
||||
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 12px;">
|
||||
<div style="padding: 22px; border-radius: 18px; background: var(--surface); border: 1px solid var(--border); box-shadow: var(--shadow);">
|
||||
<div style="font-size: 13px; color: var(--ink-3); margin-bottom: 10px;">Приглашено</div>
|
||||
<div style="font-size: 30px; font-weight: 600; letter-spacing: -0.03em; font-variant-numeric: tabular-nums;">3</div>
|
||||
</div>
|
||||
<div style="padding: 22px; border-radius: 18px; background: var(--surface); border: 1px solid var(--border); box-shadow: var(--shadow);">
|
||||
<div style="font-size: 13px; color: var(--ink-3); margin-bottom: 10px;">Месяцев начислено</div>
|
||||
<div style="font-size: 30px; font-weight: 600; letter-spacing: -0.03em; font-variant-numeric: tabular-nums;">2</div>
|
||||
</div>
|
||||
<div style="padding: 22px; border-radius: 18px; background: var(--surface); border: 1px solid var(--border); box-shadow: var(--shadow);">
|
||||
<div style="font-size: 13px; color: var(--ink-3); margin-bottom: 10px;">Ожидает подтверждения</div>
|
||||
<div style="font-size: 30px; font-weight: 600; letter-spacing: -0.03em; font-variant-numeric: tabular-nums;">1</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display: grid; gap: 14px; padding: 26px; border-radius: 20px; background: var(--surface); border: 1px solid var(--border); box-shadow: var(--shadow);">
|
||||
<div style="font-size: 16px; font-weight: 500;">Ваша ссылка</div>
|
||||
<div style="display: flex; flex-wrap: wrap; gap: 10px; align-items: center;">
|
||||
<div style="flex: 1; min-width: 220px; padding: 14px 16px; border-radius: 12px; background: var(--surface-2); border: 1px solid var(--border); font-size: 14px; color: var(--ink-2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">veilix.com/r/anna-7f2c</div>
|
||||
<button type="button" onClick="{{ copyRef }}" style="height: 46px; padding: 0 20px; border-radius: 12px; border: 0; background: var(--accent); color: var(--on-accent); font-family: inherit; font-size: 15px; cursor: pointer;" style-hover="background: var(--accent-hover);">{{ refLabel }}</button>
|
||||
</div>
|
||||
<div style="font-size: 13px; color: var(--ink-3); line-height: 1.5;">Месяц начисляется вам и другу после его первой оплаты и применяется к ближайшему списанию.</div>
|
||||
</div>
|
||||
|
||||
<div style="display: grid; gap: 10px;">
|
||||
<div style="font-size: 16px; font-weight: 500;">Приглашённые</div>
|
||||
<div style="display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 12px; padding: 16px 20px; border-radius: 16px; background: var(--surface); border: 1px solid var(--border);">
|
||||
<span style="font-size: 15px;">m···@gmail.com</span>
|
||||
<span style="font-size: 14px; color: var(--accent);">Месяц начислен</span>
|
||||
</div>
|
||||
<div style="display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 12px; padding: 16px 20px; border-radius: 16px; background: var(--surface); border: 1px solid var(--border);">
|
||||
<span style="font-size: 15px;">d···@yandex.ru</span>
|
||||
<span style="font-size: 14px; color: var(--accent);">Месяц начислен</span>
|
||||
</div>
|
||||
<div style="display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 12px; padding: 16px 20px; border-radius: 16px; background: var(--surface); border: 1px solid var(--border);">
|
||||
<span style="font-size: 15px;">k···@icloud.com</span>
|
||||
<span style="font-size: 14px; color: var(--ink-3);">Ждём оплаты</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div hidden="{{ moreOff }}" style="display: grid; gap: 16px;">
|
||||
<h1 style="margin: 0; font-size: 26px; font-weight: 600; letter-spacing: -0.03em;">Ещё</h1>
|
||||
<button type="button" onClick="{{ goBilling }}" style="display: flex; align-items: center; justify-content: space-between; gap: 12px; min-height: 56px; padding: 14px 18px; border-radius: 16px; border: 1px solid var(--border); background: var(--surface); color: var(--ink); font-family: inherit; font-size: 16px; text-align: left; cursor: pointer;">
|
||||
<span>Счёт и платежи</span>
|
||||
<span style="color: var(--ink-3);">›</span>
|
||||
</button>
|
||||
<button type="button" onClick="{{ goRef }}" style="display: flex; align-items: center; justify-content: space-between; gap: 12px; min-height: 56px; padding: 14px 18px; border-radius: 16px; border: 1px solid var(--border); background: var(--surface); color: var(--ink); font-family: inherit; font-size: 16px; text-align: left; cursor: pointer;">
|
||||
<span>Реферальная программа</span>
|
||||
<span style="color: var(--ink-3);">›</span>
|
||||
</button>
|
||||
<a href="Support.dc.html" style="display: flex; align-items: center; justify-content: space-between; gap: 12px; min-height: 56px; padding: 14px 18px; border-radius: 16px; border: 1px solid var(--border); background: var(--surface); color: var(--ink); font-size: 16px;">
|
||||
<span>Поддержка</span>
|
||||
<span style="color: var(--ink-3);">›</span>
|
||||
</a>
|
||||
<div style="display: grid; gap: 12px; padding: 14px 18px; border-radius: 16px; border: 1px solid var(--border); background: var(--surface);">
|
||||
<div style="display: flex; align-items: center; justify-content: space-between; gap: 12px;">
|
||||
<div style="font-size: 15px;">Тема</div>
|
||||
<div style="display: flex; padding: 3px; border-radius: 11px; border: 1px solid var(--border); flex: none;">
|
||||
<button type="button" onClick="{{ setLight }}" title="Светлая тема" style="display: flex; align-items: center; justify-content: center; width: 40px; height: 34px; border: 0; border-radius: 8px; background: {{ lightBg }}; color: var(--ink); cursor: pointer;">
|
||||
<span style="position: relative; width: 16px; height: 16px; display: block;">
|
||||
<span style="position: absolute; left: 4px; top: 4px; width: 8px; height: 8px; border-radius: 50%; background: currentColor; display: block;"></span>
|
||||
<span style="position: absolute; left: 0; top: 0; width: 16px; height: 16px; border-radius: 50%; border: 1.5px solid currentColor; opacity: 0.45; display: block;"></span>
|
||||
</span>
|
||||
</button>
|
||||
<button type="button" onClick="{{ setDark }}" title="Тёмная тема" style="display: flex; align-items: center; justify-content: center; width: 40px; height: 34px; border: 0; border-radius: 8px; background: {{ darkBg }}; color: var(--ink); cursor: pointer;">
|
||||
<span style="width: 16px; height: 16px; border-radius: 50%; box-shadow: inset -5px -2px 0 0 currentColor; display: block;"></span>
|
||||
</button>
|
||||
<button type="button" onClick="{{ setSystem }}" title="Как в системе" style="display: flex; align-items: center; justify-content: center; width: 40px; height: 34px; border: 0; border-radius: 8px; background: {{ systemBg }}; color: var(--ink); cursor: pointer;">
|
||||
<span style="width: 16px; height: 16px; border-radius: 50%; border: 1.5px solid currentColor; background: linear-gradient(90deg, currentColor 50%, transparent 50%); display: block;"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: flex; align-items: center; justify-content: space-between; gap: 12px; padding-top: 12px; border-top: 1px solid var(--border);">
|
||||
<div style="font-size: 15px;">Язык</div>
|
||||
<div style="display: flex; padding: 3px; border-radius: 11px; border: 1px solid var(--border); flex: none;">
|
||||
<button type="button" onClick="{{ setRu }}" style="height: 34px; padding: 0 16px; border: 0; border-radius: 8px; background: {{ ruBg }}; color: {{ ruInk }}; font-family: inherit; font-size: 14px; cursor: pointer;">RU</button>
|
||||
<button type="button" onClick="{{ setEn }}" style="height: 34px; padding: 0 16px; border: 0; border-radius: 8px; background: {{ enBg }}; color: {{ enInk }}; font-family: inherit; font-size: 14px; cursor: pointer;">EN</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" onClick="{{ signOut }}" style="justify-self: start; min-height: 48px; padding: 0 16px; border-radius: 12px; border: 1px solid var(--border); background: transparent; color: var(--ink-2); font-family: inherit; font-size: 15px; cursor: pointer;">Выйти · anna@example.com</button>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<div hidden="{{ pillOff }}" style="position: fixed; left: 50%; bottom: 18px; transform: translateX(-50%); z-index: 50; display: flex; gap: 4px; padding: 6px; border-radius: 999px; background: color-mix(in oklab, var(--surface) 62%, transparent); backdrop-filter: blur(24px) saturate(1.8); -webkit-backdrop-filter: blur(24px) saturate(1.8); border: 1px solid color-mix(in oklab, var(--ink) 8%, transparent); box-shadow: 0 10px 30px -10px color-mix(in oklab, var(--ink) 40%, transparent);">
|
||||
<button type="button" onClick="{{ goHome }}" style="display: grid; justify-items: center; gap: 4px; min-width: 76px; min-height: 52px; padding: 8px 12px; border: 0; border-radius: 999px; background: {{ tabHomeBg }}; color: {{ tabHomeInk }}; font-family: inherit; font-size: 11px; cursor: pointer;">
|
||||
<span style="position: relative; width: 20px; height: 20px; display: block;">
|
||||
<span style="position: absolute; left: 0; top: 0; width: 9px; height: 9px; border-radius: 2px; background: currentColor; display: block;"></span>
|
||||
<span style="position: absolute; right: 0; top: 0; width: 9px; height: 9px; border-radius: 2px; background: currentColor; opacity: 0.45; display: block;"></span>
|
||||
<span style="position: absolute; left: 0; bottom: 0; width: 9px; height: 9px; border-radius: 2px; background: currentColor; opacity: 0.45; display: block;"></span>
|
||||
<span style="position: absolute; right: 0; bottom: 0; width: 9px; height: 9px; border-radius: 2px; background: currentColor; display: block;"></span>
|
||||
</span>
|
||||
Главная
|
||||
</button>
|
||||
<button type="button" onClick="{{ goKeys }}" style="display: grid; justify-items: center; gap: 4px; min-width: 76px; min-height: 52px; padding: 8px 12px; border: 0; border-radius: 999px; background: {{ tabKeysBg }}; color: {{ tabKeysInk }}; font-family: inherit; font-size: 11px; cursor: pointer;">
|
||||
<span style="position: relative; width: 20px; height: 20px; display: block;">
|
||||
<span style="position: absolute; left: 0; top: 2px; width: 20px; height: 4px; border-radius: 2px; background: currentColor; display: block;"></span>
|
||||
<span style="position: absolute; left: 0; top: 9px; width: 20px; height: 4px; border-radius: 2px; background: currentColor; opacity: 0.6; display: block;"></span>
|
||||
<span style="position: absolute; left: 0; top: 16px; width: 20px; height: 4px; border-radius: 2px; background: currentColor; opacity: 0.3; display: block;"></span>
|
||||
</span>
|
||||
Ключи
|
||||
</button>
|
||||
<button type="button" onClick="{{ goBuy }}" style="display: grid; justify-items: center; gap: 4px; min-width: 76px; min-height: 52px; padding: 8px 12px; border: 0; border-radius: 999px; background: {{ tabBuyBg }}; color: {{ tabBuyInk }}; font-family: inherit; font-size: 11px; cursor: pointer;">
|
||||
<span style="position: relative; width: 20px; height: 20px; display: block;">
|
||||
<span style="position: absolute; left: 8px; top: 0; width: 4px; height: 20px; border-radius: 2px; background: currentColor; display: block;"></span>
|
||||
<span style="position: absolute; left: 0; top: 8px; width: 20px; height: 4px; border-radius: 2px; background: currentColor; display: block;"></span>
|
||||
</span>
|
||||
Купить
|
||||
</button>
|
||||
<button type="button" onClick="{{ goMore }}" style="display: grid; justify-items: center; gap: 4px; min-width: 76px; min-height: 52px; padding: 8px 12px; border: 0; border-radius: 999px; background: {{ tabMoreBg }}; color: {{ tabMoreInk }}; font-family: inherit; font-size: 11px; cursor: pointer;">
|
||||
<span style="position: relative; width: 20px; height: 20px; display: block;">
|
||||
<span style="position: absolute; left: 0; top: 8px; width: 4px; height: 4px; border-radius: 50%; background: currentColor; display: block;"></span>
|
||||
<span style="position: absolute; left: 8px; top: 8px; width: 4px; height: 4px; border-radius: 50%; background: currentColor; display: block;"></span>
|
||||
<span style="position: absolute; left: 16px; top: 8px; width: 4px; height: 4px; border-radius: 50%; background: currentColor; display: block;"></span>
|
||||
</span>
|
||||
Ещё
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div hidden="{{ qrOff }}" onClick="{{ closeQr }}" style="position: fixed; inset: 0; z-index: 60; display: flex; align-items: center; justify-content: center; padding: 24px; background: color-mix(in oklab, var(--ink) 45%, transparent); backdrop-filter: blur(6px);">
|
||||
<div style="display: grid; gap: 16px; justify-items: center; padding: 28px; border-radius: 22px; background: var(--surface); border: 1px solid var(--border); box-shadow: var(--shadow);">
|
||||
<div style="font-size: 16px; font-weight: 500;">{{ qrTitle }}</div>
|
||||
{{ qrImage }}
|
||||
<div style="max-width: 260px; text-align: center; font-size: 13px; line-height: 1.5; color: var(--ink-3);">Откройте приложение на телефоне и наведите камеру на код.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</x-dc>
|
||||
<script type="text/x-dc" data-dc-script data-props="{"defaultTheme":{"editor":"enum","options":["light","dark","system"],"default":"light","tsType":"'light' | 'dark' | 'system'","section":"Тема"}}">
|
||||
class Component extends DCLogic {
|
||||
state = { theme: null, sysDark: false, view: "home", key: 0, faq: 0, loc: 0, lead: false, leadSent: false, qr: null, copied: false, signedIn: true, otp: false, lang: "ru", narrow: false, refCopied: false, topup: false, sum: 1, method: 0, topupSent: false };
|
||||
|
||||
data() {
|
||||
const base = { setup: "4 900 ₽", month: "2 499 ₽" };
|
||||
const rare = { setup: "6 900 ₽", month: "3 499 ₽" };
|
||||
const far = { setup: "9 900 ₽", month: "4 999 ₽" };
|
||||
const L = (country, city, code, lng, lat, ready, tier) => ({ country, city, code, lng, lat, ready, setup: tier.setup, month: tier.month, rare: tier !== base });
|
||||
return [
|
||||
L("Нидерланды", "Амстердам", "nl", 4.9, 52.37, true, base),
|
||||
L("Германия", "Франкфурт", "de", 8.68, 50.11, true, base),
|
||||
L("США", "Нью-Йорк", "us", -74.01, 40.71, true, base),
|
||||
L("Великобритания", "Лондон", "gb", -0.13, 51.51, true, base),
|
||||
L("Франция", "Париж", "fr", 2.35, 48.86, true, base),
|
||||
L("Финляндия", "Хельсинки", "fi", 24.94, 60.17, true, base),
|
||||
L("Швеция", "Стокгольм", "se", 18.07, 59.33, true, base),
|
||||
L("Норвегия", "Осло", "no", 10.75, 59.91, false, base),
|
||||
L("Дания", "Копенгаген", "dk", 12.57, 55.68, false, base),
|
||||
L("Швейцария", "Цюрих", "ch", 8.54, 47.37, true, rare),
|
||||
L("Австрия", "Вена", "at", 16.37, 48.21, true, base),
|
||||
L("Испания", "Мадрид", "es", -3.7, 40.42, true, base),
|
||||
L("Италия", "Милан", "it", 9.19, 45.46, true, base),
|
||||
L("Польша", "Варшава", "pl", 21.01, 52.23, true, base),
|
||||
L("Чехия", "Прага", "cz", 14.44, 50.08, true, base),
|
||||
L("Румыния", "Бухарест", "ro", 26.1, 44.43, false, base),
|
||||
L("Литва", "Вильнюс", "lt", 25.28, 54.69, true, base),
|
||||
L("Латвия", "Рига", "lv", 24.11, 56.95, true, base),
|
||||
L("Эстония", "Таллин", "ee", 24.75, 59.44, false, base),
|
||||
L("Турция", "Стамбул", "tr", 28.98, 41.01, true, base),
|
||||
L("Казахстан", "Алматы", "kz", 76.89, 43.24, false, base),
|
||||
L("Армения", "Ереван", "am", 44.51, 40.18, false, base),
|
||||
L("Грузия", "Тбилиси", "ge", 44.8, 41.72, false, base),
|
||||
L("ОАЭ", "Дубай", "ae", 55.27, 25.2, true, rare),
|
||||
L("Сингапур", "Сингапур", "sg", 103.82, 1.35, true, rare),
|
||||
L("Япония", "Токио", "jp", 139.69, 35.69, true, rare),
|
||||
L("Южная Корея", "Сеул", "kr", 126.98, 37.57, false, rare),
|
||||
L("Канада", "Торонто", "ca", -79.38, 43.65, true, base),
|
||||
L("Австралия", "Сидней", "au", 151.21, -33.87, false, far),
|
||||
L("Бразилия", "Сан-Паулу", "br", -46.63, -23.55, false, far)
|
||||
];
|
||||
}
|
||||
|
||||
flagSpecs() {
|
||||
return {
|
||||
nl: { rows: ["#AE1C28", "#FFFFFF", "#21468B"] },
|
||||
de: { rows: ["#000000", "#DD0000", "#FFCE00"] },
|
||||
us: { rows: ["#B22234", "#FFFFFF", "#B22234", "#FFFFFF", "#B22234", "#FFFFFF"], marks: [{ l: 0, t: 0, w: 22, h: 17, bg: "#3C3B6E" }] },
|
||||
gb: { bg: "#012169", marks: [{ l: 17, t: 0, w: 12, h: 32, bg: "#FFFFFF" }, { l: 0, t: 10, w: 46, h: 12, bg: "#FFFFFF" }, { l: 20, t: 0, w: 6, h: 32, bg: "#C8102E" }, { l: 0, t: 13, w: 46, h: 6, bg: "#C8102E" }] },
|
||||
fr: { cols: ["#002395", "#FFFFFF", "#ED2939"] },
|
||||
fi: { bg: "#FFFFFF", marks: [{ l: 13, t: 0, w: 8, h: 32, bg: "#003580" }, { l: 0, t: 12, w: 46, h: 8, bg: "#003580" }] },
|
||||
se: { bg: "#006AA7", marks: [{ l: 13, t: 0, w: 6, h: 32, bg: "#FECC00" }, { l: 0, t: 13, w: 46, h: 6, bg: "#FECC00" }] },
|
||||
no: { bg: "#BA0C2F", marks: [{ l: 12, t: 0, w: 10, h: 32, bg: "#FFFFFF" }, { l: 0, t: 11, w: 46, h: 10, bg: "#FFFFFF" }, { l: 15, t: 0, w: 4, h: 32, bg: "#00205B" }, { l: 0, t: 14, w: 46, h: 4, bg: "#00205B" }] },
|
||||
dk: { bg: "#C8102E", marks: [{ l: 13, t: 0, w: 6, h: 32, bg: "#FFFFFF" }, { l: 0, t: 13, w: 46, h: 6, bg: "#FFFFFF" }] },
|
||||
ch: { bg: "#D52B1E", marks: [{ l: 20, t: 8, w: 6, h: 16, bg: "#FFFFFF" }, { l: 15, t: 13, w: 16, h: 6, bg: "#FFFFFF" }] },
|
||||
at: { rows: ["#ED2939", "#FFFFFF", "#ED2939"] },
|
||||
es: { rows: ["#AA151B", "#F1BF00", "#AA151B"], sizes: "1fr 2fr 1fr" },
|
||||
it: { cols: ["#008C45", "#F4F5F0", "#CD212A"] },
|
||||
pl: { rows: ["#FFFFFF", "#DC143C"] },
|
||||
cz: { rows: ["#FFFFFF", "#D7141A"], marks: [{ l: 0, t: 0, w: 18, h: 32, bg: "#11457E", clip: "polygon(0 0, 100% 50%, 0 100%)" }] },
|
||||
ro: { cols: ["#002B7F", "#FCD116", "#CE1126"] },
|
||||
lt: { rows: ["#FDB913", "#006A44", "#C1272D"] },
|
||||
lv: { rows: ["#9E3039", "#FFFFFF", "#9E3039"], sizes: "2fr 1fr 2fr" },
|
||||
ee: { rows: ["#0072CE", "#000000", "#FFFFFF"] },
|
||||
tr: { bg: "#E30A17", marks: [{ l: 11, t: 8, w: 16, h: 16, r: "50%", bg: "#FFFFFF" }, { l: 16, t: 8, w: 16, h: 16, r: "50%", bg: "#E30A17" }] },
|
||||
kz: { bg: "#00AFCA", marks: [{ l: 16, t: 8, w: 15, h: 15, r: "50%", bg: "#FEC50C" }] },
|
||||
am: { rows: ["#D90012", "#0033A0", "#F2A800"] },
|
||||
ge: { bg: "#FFFFFF", marks: [{ l: 20, t: 0, w: 6, h: 32, bg: "#FF0000" }, { l: 0, t: 13, w: 46, h: 6, bg: "#FF0000" }] },
|
||||
ae: { rows: ["#00732F", "#FFFFFF", "#000000"], marks: [{ l: 0, t: 0, w: 11, h: 32, bg: "#FF0000" }] },
|
||||
sg: { rows: ["#EF3340", "#FFFFFF"], marks: [{ l: 5, t: 3, w: 11, h: 11, r: "50%", bg: "#FFFFFF" }, { l: 9, t: 3, w: 11, h: 11, r: "50%", bg: "#EF3340" }] },
|
||||
jp: { bg: "#FFFFFF", marks: [{ l: 16, t: 9, w: 14, h: 14, r: "50%", bg: "#BC002D" }] },
|
||||
kr: { bg: "#FFFFFF", marks: [{ l: 16, t: 9, w: 14, h: 14, r: "50%", bg: "#CD2E3A" }, { l: 16, t: 9, w: 14, h: 14, r: "50%", bg: "#0047A0", clip: "polygon(0 50%, 100% 50%, 100% 100%, 0 100%)" }] },
|
||||
ca: { cols: ["#D80621", "#FFFFFF", "#D80621"], sizes: "1fr 2fr 1fr", marks: [{ l: 20, t: 9, w: 7, h: 13, bg: "#D80621", clip: "polygon(50% 0, 100% 100%, 0 100%)" }] },
|
||||
au: { bg: "#00247D", marks: [{ l: 9, t: 0, w: 4, h: 16, bg: "#FFFFFF" }, { l: 0, t: 6, w: 22, h: 4, bg: "#FFFFFF" }, { l: 30, t: 19, w: 7, h: 7, r: "50%", bg: "#FFFFFF" }] },
|
||||
br: { bg: "#009739", marks: [{ l: 9, t: 4, w: 28, h: 24, bg: "#FEDD00", clip: "polygon(50% 0, 100% 50%, 50% 100%, 0 50%)" }, { l: 18, t: 11, w: 10, h: 10, r: "50%", bg: "#012169" }] }
|
||||
};
|
||||
}
|
||||
|
||||
flag(code) {
|
||||
const h = React.createElement;
|
||||
const spec = this.flagSpecs()[code] || {};
|
||||
const style = {
|
||||
width: "46px", height: "32px", borderRadius: "6px", overflow: "hidden",
|
||||
display: "grid", border: "1px solid var(--border)", position: "relative", flex: "none"
|
||||
};
|
||||
let bands = [];
|
||||
if (spec.rows) {
|
||||
bands = spec.rows;
|
||||
style.gridTemplateRows = spec.sizes || "repeat(" + bands.length + ", 1fr)";
|
||||
} else if (spec.cols) {
|
||||
bands = spec.cols;
|
||||
style.gridAutoFlow = "column";
|
||||
style.gridTemplateColumns = spec.sizes || "repeat(" + bands.length + ", 1fr)";
|
||||
} else {
|
||||
style.background = spec.bg || "var(--surface-2)";
|
||||
}
|
||||
const kids = bands.map((c, i) => h("span", { key: "b" + i, style: { background: c, display: "block" } }));
|
||||
(spec.marks || []).forEach((m, i) => kids.push(h("span", {
|
||||
key: "m" + i,
|
||||
style: {
|
||||
position: "absolute", left: m.l + "px", top: m.t + "px", width: m.w + "px", height: m.h + "px",
|
||||
borderRadius: m.r || 0, background: m.bg, clipPath: m.clip || "none", display: "block"
|
||||
}
|
||||
})));
|
||||
return h("span", { style: style }, kids);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const mq = window.matchMedia("(prefers-color-scheme: dark)");
|
||||
this.mq = mq;
|
||||
this.onSys = () => this.setState({ sysDark: mq.matches });
|
||||
mq.addEventListener("change", this.onSys);
|
||||
this.onResize = () => {
|
||||
const n = window.innerWidth < 900;
|
||||
if (n !== this.state.narrow) this.setState({ narrow: n, view: this.state.view === "more" && !n ? "home" : this.state.view });
|
||||
if (!n && this.state.view === "more") this.setState({ view: "home" });
|
||||
};
|
||||
window.addEventListener("resize", this.onResize);
|
||||
this.onResize();
|
||||
let saved = null;
|
||||
try { saved = localStorage.getItem("veilix.theme"); } catch (e) {}
|
||||
let savedLang = null;
|
||||
try { savedLang = localStorage.getItem("veilix.lang"); } catch (e) {}
|
||||
this.setState({ sysDark: mq.matches, theme: saved || this.props.defaultTheme || "light", lang: savedLang || "ru" }, () => { this.apply(); this.applyLang(); });
|
||||
}
|
||||
|
||||
applyLang() {
|
||||
const lang = this.state.lang || "ru";
|
||||
if (window.VEILIX_APPLY_LANG) window.VEILIX_APPLY_LANG(document.body, lang);
|
||||
else setTimeout(() => this.applyLang(), 150);
|
||||
}
|
||||
|
||||
setLang(l) {
|
||||
try { localStorage.setItem("veilix.lang", l); } catch (e) {}
|
||||
this.setState({ lang: l }, () => this.applyLang());
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.mq) this.mq.removeEventListener("change", this.onSys);
|
||||
if (this.onResize) window.removeEventListener("resize", this.onResize);
|
||||
}
|
||||
componentDidUpdate() { this.apply(); this.applyLang(); }
|
||||
|
||||
apply() {
|
||||
const theme = this.state.theme || this.props.defaultTheme || "light";
|
||||
const dark = theme === "dark" || (theme === "system" && this.state.sysDark);
|
||||
document.documentElement.setAttribute("data-veilix-theme", dark ? "dark" : "light");
|
||||
}
|
||||
|
||||
setTheme(t) {
|
||||
try { localStorage.setItem("veilix.theme", t); } catch (e) {}
|
||||
this.setState({ theme: t }, () => this.apply());
|
||||
}
|
||||
|
||||
qrImage(seed) {
|
||||
const n = 21;
|
||||
let h = 0;
|
||||
for (let i = 0; i < seed.length; i++) h = (h * 31 + seed.charCodeAt(i)) >>> 0;
|
||||
const cells = [];
|
||||
const finder = (r, c) => (r < 7 && c < 7) || (r < 7 && c >= n - 7) || (r >= n - 7 && c < 7);
|
||||
for (let r = 0; r < n; r++) {
|
||||
for (let c = 0; c < n; c++) {
|
||||
let on;
|
||||
if (finder(r, c)) {
|
||||
const rr = r < 7 ? r : n - 1 - r;
|
||||
const cc = c < 7 ? c : n - 1 - c;
|
||||
const d = Math.max(Math.abs(rr - 3), Math.abs(cc - 3));
|
||||
on = d !== 2;
|
||||
} else {
|
||||
h = (h * 1103515245 + 12345) >>> 0;
|
||||
on = ((h >>> 16) & 1) === 1;
|
||||
}
|
||||
cells.push(React.createElement("span", {
|
||||
key: r + "-" + c,
|
||||
style: { background: on ? "var(--ink)" : "transparent", display: "block" }
|
||||
}));
|
||||
}
|
||||
}
|
||||
return React.createElement("div", {
|
||||
style: {
|
||||
display: "grid",
|
||||
gridTemplateColumns: "repeat(" + n + ", 1fr)",
|
||||
width: "220px",
|
||||
height: "220px",
|
||||
padding: "12px",
|
||||
borderRadius: "14px",
|
||||
background: "var(--bg)",
|
||||
border: "1px solid var(--border)"
|
||||
}
|
||||
}, cells);
|
||||
}
|
||||
|
||||
renderVals() {
|
||||
const s = this.state;
|
||||
const sel = "var(--accent-soft)";
|
||||
const theme = s.theme || this.props.defaultTheme || "light";
|
||||
const keyData = [
|
||||
{ title: "Нидерланды · Амстердам", ip: "185.72.14.203", until: s.lang === "en" ? "14 October 2026" : "14 октября 2026", badge: "Активен", warn: false, devices: "3 из 5 устройств", payment: "Карта ···· 4242", card: "···· 4242", left: "Продлится автоматически через 31 день", xray: "vless://8f2a…@nl1.veilix.net:443?type=xhttp#Veilix-NL" },
|
||||
{ title: "США · Нью-Йорк", ip: "23.105.61.44", until: s.lang === "en" ? "21 September 2026" : "21 сентября 2026", badge: "Истекает", warn: true, devices: "1 из 5 устройств", payment: "Карта ···· 8801", card: "···· 8801", left: "Осталось 8 дней", xray: "vless://c41d…@us1.veilix.net:443?type=xhttp#Veilix-US" }
|
||||
];
|
||||
const cur = keyData[s.key];
|
||||
const mk = (i) => ({
|
||||
title: keyData[i].title,
|
||||
ip: keyData[i].ip,
|
||||
until: keyData[i].until,
|
||||
untilLine: (s.lang === "en" ? "until " : "до ") + keyData[i].until,
|
||||
badge: keyData[i].badge,
|
||||
devices: keyData[i].devices,
|
||||
payment: keyData[i].payment,
|
||||
badgeBg: keyData[i].warn ? "color-mix(in oklab, var(--warn) 18%, transparent)" : sel,
|
||||
badgeInk: keyData[i].warn ? "var(--warn)" : "var(--accent)",
|
||||
open: () => this.setState({ view: "key", key: i })
|
||||
});
|
||||
const faqData = [
|
||||
["Как подключить новое устройство?", "Откройте ключ, скачайте любую свободную конфигурацию WireGuard или отсканируйте её QR-код. Каждая конфигурация рассчитана на одно устройство."],
|
||||
["Что делать, если подключение перестало работать?", "Переключитесь на Xray в том же ключе — он настроен заранее. Если не помогло, напишите в поддержку, ответим в течение часа."],
|
||||
["Как отменить продление?", "На странице ключа нажмите «Изменить карту» и отвяжите её. Ключ доработает оплаченный месяц."]
|
||||
];
|
||||
const locData = [
|
||||
["Нидерланды", "Амстердам", true],
|
||||
["Германия", "Франкфурт", true],
|
||||
["США", "Нью-Йорк", true],
|
||||
["Финляндия", "Хельсинки", false],
|
||||
["Турция", "Стамбул", false],
|
||||
["Япония", "Токио", false]
|
||||
];
|
||||
const cfgNames = [
|
||||
["Ноутбук", "подключено"],
|
||||
["Телефон", "подключено"],
|
||||
["Планшет", "подключено"],
|
||||
["Рабочий ноутбук", "не занят"],
|
||||
["Свободный слот", "не занят"]
|
||||
];
|
||||
|
||||
return {
|
||||
appOff: s.signedIn,
|
||||
loginOff: !s.signedIn,
|
||||
otpSent: s.otp,
|
||||
otpNotSent: !s.otp,
|
||||
sendOtp: () => this.setState({ otp: true }),
|
||||
signIn: () => this.setState({ signedIn: true, otp: false, view: "home" }),
|
||||
backToEmail: () => this.setState({ otp: false }),
|
||||
signOut: () => this.setState({ signedIn: false, otp: false }),
|
||||
|
||||
goHome: () => this.setState({ view: "home" }),
|
||||
goKeys: () => this.setState({ view: "keys" }),
|
||||
goBuy: () => this.setState({ view: "buy" }),
|
||||
goMore: () => this.setState({ view: "more" }),
|
||||
goBilling: () => this.setState({ view: "billing" }),
|
||||
billingOff: s.view !== "billing",
|
||||
billingBg: s.view === "billing" ? sel : "transparent",
|
||||
billingInk: s.view === "billing" ? "var(--accent)" : "var(--ink-2)",
|
||||
openTopup: () => this.setState({ topup: true }),
|
||||
closeTopup: () => this.setState({ topup: false }),
|
||||
topupOff: !s.topup,
|
||||
sums: ["2 500 ₽", "5 000 ₽", "10 000 ₽", "Своя сумма"].map((label, i) => ({
|
||||
label: label,
|
||||
bg: s.sum === i ? sel : "transparent",
|
||||
border: s.sum === i ? "var(--accent)" : "var(--border)",
|
||||
pick: () => this.setState({ sum: i })
|
||||
})),
|
||||
methods: ["Карта ···· 4242", "СБП", "Криптовалюта"].map((label, i) => ({
|
||||
label: label,
|
||||
bg: s.method === i ? sel : "transparent",
|
||||
border: s.method === i ? "var(--accent)" : "var(--border)",
|
||||
pick: () => this.setState({ method: i })
|
||||
})),
|
||||
cryptoOff: s.method !== 2,
|
||||
sendTopup: () => this.setState({ topupSent: true }),
|
||||
topupButton: s.topupSent ? "Счёт выставлен" : "Пополнить",
|
||||
payments: [
|
||||
{ date: "14 сентября", title: "Продление · Нидерланды · Амстердам", method: "Карта ···· 4242", status: "Оплачено", amount: "−2 499 ₽", ink: "var(--ink)" },
|
||||
{ date: "6 сентября", title: "Пополнение баланса", method: "USDT (TRC-20)", status: "Зачислено", amount: "+5 000 ₽", ink: "var(--accent)" },
|
||||
{ date: "21 августа", title: "Первый месяц · США · Нью-Йорк", method: "Карта ···· 4242", status: "Оплачено", amount: "−4 900 ₽", ink: "var(--ink)" },
|
||||
{ date: "14 августа", title: "Продление · Нидерланды · Амстердам", method: "С баланса", status: "Оплачено", amount: "−2 499 ₽", ink: "var(--ink)" },
|
||||
{ date: "2 августа", title: "Реферальный бонус", method: "Месяц в подарок", status: "Начислено", amount: "+2 499 ₽", ink: "var(--accent)" }
|
||||
],
|
||||
goRef: () => this.setState({ view: "ref" }),
|
||||
refOff: s.view !== "ref",
|
||||
refBg: s.view === "ref" ? sel : "transparent",
|
||||
refInk: s.view === "ref" ? "var(--accent)" : "var(--ink-2)",
|
||||
setRu: () => this.setLang("ru"),
|
||||
setEn: () => this.setLang("en"),
|
||||
ruBg: s.lang === "en" ? "transparent" : sel,
|
||||
enBg: s.lang === "en" ? sel : "transparent",
|
||||
ruInk: s.lang === "en" ? "var(--ink-3)" : "var(--ink)",
|
||||
enInk: s.lang === "en" ? "var(--ink)" : "var(--ink-3)",
|
||||
homeOff: s.view !== "home",
|
||||
keysOff: s.view !== "keys",
|
||||
keyOff: s.view !== "key",
|
||||
buyOff: s.view !== "buy",
|
||||
moreOff: s.view !== "more",
|
||||
shellCols: s.narrow ? "minmax(0, 1fr)" : "248px minmax(0, 1fr)",
|
||||
mainPad: s.narrow ? "20px 16px 110px" : "36px 40px 64px",
|
||||
asideOff: s.narrow,
|
||||
topbarOff: !s.narrow,
|
||||
pillOff: !s.narrow,
|
||||
tabHomeBg: s.view === "home" ? sel : "transparent",
|
||||
tabKeysBg: (s.view === "keys" || s.view === "key") ? sel : "transparent",
|
||||
tabBuyBg: s.view === "buy" ? sel : "transparent",
|
||||
tabMoreBg: s.view === "more" ? sel : "transparent",
|
||||
tabHomeInk: s.view === "home" ? "var(--accent)" : "var(--ink-2)",
|
||||
tabKeysInk: (s.view === "keys" || s.view === "key") ? "var(--accent)" : "var(--ink-2)",
|
||||
tabBuyInk: s.view === "buy" ? "var(--accent)" : "var(--ink-2)",
|
||||
tabMoreInk: s.view === "more" ? "var(--accent)" : "var(--ink-2)",
|
||||
homeBg: s.view === "home" ? sel : "transparent",
|
||||
keysBg: (s.view === "keys" || s.view === "key") ? sel : "transparent",
|
||||
buyBg: s.view === "buy" ? sel : "transparent",
|
||||
homeInk: s.view === "home" ? "var(--accent)" : "var(--ink-2)",
|
||||
keysInk: (s.view === "keys" || s.view === "key") ? "var(--accent)" : "var(--ink-2)",
|
||||
buyInk: s.view === "buy" ? "var(--accent)" : "var(--ink-2)",
|
||||
|
||||
keys: [mk(0), mk(1)],
|
||||
allKeys: [mk(0), mk(1)],
|
||||
cur: {
|
||||
title: cur.title,
|
||||
ip: cur.ip,
|
||||
until: cur.until,
|
||||
card: cur.card,
|
||||
devices: cur.devices,
|
||||
left: cur.left,
|
||||
leftInk: cur.warn ? "var(--warn)" : "var(--ink-2)",
|
||||
xray: cur.xray
|
||||
},
|
||||
configs: cfgNames.map((c, i) => ({
|
||||
name: c[0],
|
||||
state: c[1],
|
||||
showQr: () => this.setState({ qr: { title: "WireGuard · " + c[0], seed: cur.ip + c[0] } })
|
||||
})),
|
||||
refStats: s.lang === "en" ? "Invited: 3 · Months earned: 2" : "Приглашено: 3 · Месяцев начислено: 2",
|
||||
copyRef: () => this.setState({ refCopied: true }),
|
||||
refLabel: s.refCopied ? "Ссылка скопирована" : "Копировать ссылку",
|
||||
copyXray: () => this.setState({ copied: true }),
|
||||
copyLabel: s.copied ? "Скопировано" : "Копировать",
|
||||
showXrayQr: () => this.setState({ qr: { title: "Xray · Happ", seed: cur.xray } }),
|
||||
qrOff: !s.qr,
|
||||
qrTitle: s.qr ? s.qr.title : "",
|
||||
qrImage: s.qr ? this.qrImage(s.qr.seed) : null,
|
||||
closeQr: () => this.setState({ qr: null }),
|
||||
|
||||
faq: faqData.map((row, i) => ({
|
||||
q: row[0],
|
||||
a: row[1],
|
||||
off: s.faq !== i,
|
||||
rot: s.faq === i ? "rotate(-135deg) translateY(2px)" : "rotate(45deg) translateY(-2px)",
|
||||
toggle: () => this.setState(st => ({ faq: st.faq === i ? -1 : i }))
|
||||
})),
|
||||
|
||||
locations: this.data().map((d, i) => ({
|
||||
country: d.country,
|
||||
city: d.city,
|
||||
flag: this.flag(d.code),
|
||||
status: d.ready ? "Серверы в наличии" : "Установим за 24 часа",
|
||||
statusInk: d.ready ? "var(--accent)" : "var(--ink-3)",
|
||||
bg: s.loc === i ? sel : "var(--surface)",
|
||||
border: s.loc === i ? "var(--accent)" : "var(--border)",
|
||||
dot: s.loc === i ? "var(--accent)" : "transparent",
|
||||
dotOff: s.loc === i,
|
||||
setup: d.setup,
|
||||
month: d.month,
|
||||
buyOff: s.loc !== i,
|
||||
pick: () => this.setState({ loc: i })
|
||||
})),
|
||||
openLead: () => this.setState({ lead: true }),
|
||||
sendLead: () => this.setState({ leadSent: true }),
|
||||
leadOpen: s.lead,
|
||||
leadClosed: !s.lead,
|
||||
leadButton: s.leadSent ? "Заявка отправлена" : "Отправить",
|
||||
pickedTitle: locData[s.loc][0] + " · " + locData[s.loc][1],
|
||||
pickedHint: locData[s.loc][2] ? "Доступ выдадим сразу после оплаты" : "Установим сервер под вас, доступ в течение 24 часов",
|
||||
|
||||
toggleLang: () => this.setLang(s.lang === "ru" ? "en" : "ru"),
|
||||
langTitle: s.lang === "ru" ? "Язык: русский" : "Язык: английский",
|
||||
langLabel: s.lang === "ru" ? "RU" : "EN",
|
||||
setLight: () => this.setTheme("light"),
|
||||
setDark: () => this.setTheme("dark"),
|
||||
setSystem: () => this.setTheme("system"),
|
||||
lightBg: theme === "light" ? sel : "transparent",
|
||||
darkBg: theme === "dark" ? sel : "transparent",
|
||||
systemBg: theme === "system" ? sel : "transparent",
|
||||
lightInk: theme === "light" ? "var(--ink)" : "var(--ink-3)",
|
||||
darkInk: theme === "dark" ? "var(--ink)" : "var(--ink-3)",
|
||||
systemInk: theme === "system" ? "var(--ink)" : "var(--ink-3)"
|
||||
};
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,815 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<script src="./support.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<x-dc>
|
||||
<helmet>
|
||||
<link rel="icon" type="image/svg+xml" href="favicon.svg" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin="anonymous" />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Commissioner:wght@300;400;500;600&family=Unbounded:wght@500;600&display=swap" rel="stylesheet" />
|
||||
<script>(function(){if(window.__veilixI18nSrc)return;window.__veilixI18nSrc="./i18n.js?v="+Date.now().toString(36)+Math.random().toString(36).slice(2,8);var s=document.createElement("script");s.src=window.__veilixI18nSrc;document.head.appendChild(s);})();</script>
|
||||
<style>
|
||||
:root{
|
||||
--bg: oklch(99% 0.005 296);
|
||||
--surface: oklch(100% 0 0);
|
||||
--surface-2: oklch(97.5% 0.01 296);
|
||||
--ink: oklch(24% 0.04 296);
|
||||
--ink-2: oklch(45% 0.03 296);
|
||||
--ink-3: oklch(60% 0.025 296);
|
||||
--border: oklch(92% 0.014 296);
|
||||
--accent: oklch(45% 0.17 296);
|
||||
--accent-soft: oklch(95.5% 0.028 296);
|
||||
--on-accent: oklch(99% 0.01 296);
|
||||
--glow-3: oklch(91% 0.08 332);
|
||||
--veil: oklch(100% 0 0 / 0.55);
|
||||
--warn: oklch(62% 0.14 62);
|
||||
--tg: oklch(62% 0.15 250);
|
||||
}
|
||||
:root[data-veilix-theme="dark"]{
|
||||
--bg: oklch(17% 0.022 296);
|
||||
--surface: oklch(21% 0.026 296);
|
||||
--surface-2: oklch(24% 0.03 296);
|
||||
--ink: oklch(96% 0.01 296);
|
||||
--ink-2: oklch(79% 0.02 296);
|
||||
--ink-3: oklch(63% 0.025 296);
|
||||
--border: oklch(31% 0.032 296);
|
||||
--accent: oklch(74% 0.15 296);
|
||||
--accent-soft: oklch(27% 0.05 296);
|
||||
--on-accent: oklch(17% 0.03 296);
|
||||
--glow-3: oklch(42% 0.13 332);
|
||||
--veil: oklch(100% 0 0 / 0.07);
|
||||
--warn: oklch(78% 0.13 62);
|
||||
--tg: oklch(70% 0.13 250);
|
||||
}
|
||||
*{ box-sizing: border-box; }
|
||||
input, textarea, select{ font-size: 16px; }
|
||||
[hidden]{ display: none !important; }
|
||||
body{ margin:0; background: oklch(95% 0.008 296); color: var(--ink); font-family:"Commissioner", system-ui, sans-serif; -webkit-font-smoothing: antialiased; }
|
||||
:root[data-veilix-theme="dark"] body{ background: oklch(13% 0.018 296); }
|
||||
a{ color: var(--accent); text-decoration: none; }
|
||||
input::placeholder, textarea::placeholder{ color: var(--ink-3); }
|
||||
</style>
|
||||
</helmet>
|
||||
|
||||
<template id="__bundler_thumbnail" data-bg-color="#efeaf8">
|
||||
<svg viewBox="0 0 1200 800" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="480" y="290" width="150" height="150" rx="48" fill="#5b32c9" />
|
||||
<rect x="570" y="360" width="150" height="150" rx="48" fill="#ffffff" fill-opacity="0.62" stroke="#5b32c9" stroke-width="6" />
|
||||
</svg>
|
||||
</template>
|
||||
<div style="min-height: 100vh; display: flex; flex-wrap: wrap; align-items: flex-start; justify-content: center; gap: 32px; padding: 40px 24px 64px;">
|
||||
|
||||
<x-import component-from-global-scope="IOSDevice" from="./ios-frame.jsx?v=20260915" hint-size="393px,852px">
|
||||
<div style="display: flex; flex-direction: column; height: 100%; padding-top: 62px; padding-bottom: 34px; background: var(--bg); color: var(--ink); font-family: 'Commissioner', system-ui, sans-serif; letter-spacing: -0.01em;">
|
||||
|
||||
<div style="display: flex; align-items: center; gap: 12px; padding: 10px 16px; border-bottom: 1px solid var(--border); background: var(--surface);">
|
||||
<span style="position: relative; width: 24px; height: 24px; display: block; flex: none;">
|
||||
<span style="position: absolute; left: 0; top: 3px; width: 18px; height: 18px; border-radius: 6px; background: linear-gradient(140deg, var(--accent), color-mix(in oklab, var(--accent) 55%, var(--glow-3))); display: block;"></span>
|
||||
<span style="position: absolute; right: 0; bottom: 3px; width: 18px; height: 18px; border-radius: 6px; background: var(--veil); border: 1px solid color-mix(in oklab, var(--accent) 35%, transparent); display: block;"></span>
|
||||
</span>
|
||||
<span style="display: grid; gap: 1px; flex: 1; min-width: 0;">
|
||||
<span style="font-size: 15px; font-weight: 500; letter-spacing: -0.02em;">Вейликс</span>
|
||||
<span style="font-size: 11px; color: var(--tg);">мини-приложение</span>
|
||||
</span>
|
||||
<span style="font-size: 15px; color: var(--tg);">Закрыть</span>
|
||||
</div>
|
||||
|
||||
<div style="flex: 1; min-height: 0; overflow: auto; padding: 16px 16px 108px;">
|
||||
|
||||
<div hidden="{{ buyOff }}" style="display: grid; gap: 12px;">
|
||||
<div style="display: grid; gap: 4px; padding: 2px;">
|
||||
<div style="font-size: 22px; font-weight: 600; letter-spacing: -0.03em;">Ваш личный VPN</div>
|
||||
<div style="font-size: 13px; color: var(--ink-2);">От 4 900 ₽ первый месяц, далее от 2 499 ₽.</div>
|
||||
</div>
|
||||
|
||||
<div style="display: grid; gap: 9px; padding: 16px; border-radius: 18px; background: var(--surface); border: 1px solid var(--border);">
|
||||
<div style="display: flex; gap: 10px; font-size: 14px; line-height: 1.45;">
|
||||
<span style="width: 6px; height: 6px; margin-top: 7px; border-radius: 50%; background: var(--accent); display: block; flex: none;"></span>
|
||||
<span>Индивидуальный IP в выбранной стране</span>
|
||||
</div>
|
||||
<div style="display: flex; gap: 10px; font-size: 14px; line-height: 1.45;">
|
||||
<span style="width: 6px; height: 6px; margin-top: 7px; border-radius: 50%; background: var(--accent); display: block; flex: none;"></span>
|
||||
<span>Выделенный RU-адрес в комплекте</span>
|
||||
</div>
|
||||
<div style="display: flex; gap: 10px; font-size: 14px; line-height: 1.45;">
|
||||
<span style="width: 6px; height: 6px; margin-top: 7px; border-radius: 50%; background: var(--accent); display: block; flex: none;"></span>
|
||||
<span>До 5 устройств</span>
|
||||
</div>
|
||||
<div style="display: flex; gap: 10px; font-size: 14px; line-height: 1.45;">
|
||||
<span style="width: 6px; height: 6px; margin-top: 7px; border-radius: 50%; background: var(--accent); display: block; flex: none;"></span>
|
||||
<span style="display: grid; gap: 5px;">
|
||||
<span>2 типа подключения:</span>
|
||||
<span style="display: flex; gap: 8px; color: var(--ink-2);">
|
||||
<span style="flex: none;">—</span>
|
||||
<span>WireGuard / Amnezia WG</span>
|
||||
</span>
|
||||
<span style="display: flex; gap: 8px; color: var(--ink-2);">
|
||||
<span style="flex: none;">—</span>
|
||||
<span>Xray: VLESS, Hysteria2, XHTTP</span>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; align-items: baseline; justify-content: space-between; gap: 10px; padding: 8px 2px 0;">
|
||||
<span style="font-size: 16px; font-weight: 500;">Локации</span>
|
||||
<span style="font-size: 12px; color: var(--ink-3);">{{ locCount }}</span>
|
||||
</div>
|
||||
|
||||
<sc-for list="{{ locations }}" as="loc" hint-placeholder-count="8">
|
||||
<div style="display: grid; gap: 11px; padding: 14px; border-radius: 16px; background: var(--surface); border: 1px solid var(--border);">
|
||||
<div style="display: flex; align-items: center; gap: 11px;">
|
||||
{{ loc.flag }}
|
||||
<span style="flex: 1; min-width: 0; display: grid; gap: 2px;">
|
||||
<span style="font-size: 15px; font-weight: 500;">{{ loc.country }}</span>
|
||||
<span style="font-size: 12px; color: {{ loc.statusInk }};">{{ loc.sub }}</span>
|
||||
</span>
|
||||
<button type="button" onClick="{{ loc.buy }}" style="flex: none; min-height: 44px; padding: 0 16px; border-radius: 12px; border: 0; background: {{ loc.btnBg }}; color: {{ loc.btnInk }}; font-family: inherit; font-size: 14px; font-weight: 500; cursor: pointer;">{{ loc.btn }}</button>
|
||||
</div>
|
||||
<div style="display: flex; flex-wrap: wrap; align-items: baseline; gap: 2px 6px; padding-top: 11px; border-top: 1px solid var(--border); font-size: 12px; color: var(--ink-2); font-variant-numeric: tabular-nums;">
|
||||
<span style="font-size: 15px; font-weight: 500; color: var(--ink);">{{ loc.setup }}</span>
|
||||
<span>первый месяц · далее</span>
|
||||
<span style="font-weight: 500; color: var(--ink);">{{ loc.month }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</sc-for>
|
||||
|
||||
<div style="display: grid; gap: 12px; margin-top: 8px; padding: 16px; border-radius: 18px; background: var(--surface-2); border: 1px solid var(--border);">
|
||||
<div style="display: grid; gap: 5px;">
|
||||
<div style="font-size: 16px; font-weight: 500;">Нужны индивидуальные условия</div>
|
||||
<div style="font-size: 13px; line-height: 1.5; color: var(--ink-2);">Команда, несколько адресов, редкая страна или свой сценарий маршрутизации. Оставьте заявку — ответим в течение дня.</div>
|
||||
</div>
|
||||
<input type="text" placeholder="Имя" style="min-height: 46px; padding: 0 14px; border-radius: 12px; border: 1px solid var(--border); background: var(--bg); color: var(--ink); font-family: inherit; width: 100%;" />
|
||||
<input type="text" placeholder="Telegram или почта" style="min-height: 46px; padding: 0 14px; border-radius: 12px; border: 1px solid var(--border); background: var(--bg); color: var(--ink); font-family: inherit; width: 100%;" />
|
||||
<textarea rows="3" placeholder="Коротко о задаче" style="padding: 12px 14px; border-radius: 12px; border: 1px solid var(--border); background: var(--bg); color: var(--ink); font-family: inherit; resize: vertical; width: 100%;"></textarea>
|
||||
<button type="button" onClick="{{ sendLead }}" style="min-height: 48px; border-radius: 12px; border: 0; background: var(--accent); color: var(--on-accent); font-family: inherit; font-size: 15px; font-weight: 500; cursor: pointer;">{{ leadButton }}</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div hidden="{{ faqOff }}" style="display: grid; gap: 10px;">
|
||||
<div style="display: grid; gap: 4px; padding: 2px 2px 6px;">
|
||||
<div style="font-size: 22px; font-weight: 600; letter-spacing: -0.03em;">Частые вопросы</div>
|
||||
<div style="font-size: 13px; color: var(--ink-2);">Если ответа нет — напишите в поддержку.</div>
|
||||
</div>
|
||||
<sc-for list="{{ faq }}" as="item" hint-placeholder-count="6">
|
||||
<div style="border: 1px solid var(--border); border-radius: 16px; background: var(--surface); overflow: hidden;">
|
||||
<button type="button" onClick="{{ item.toggle }}" style="display: flex; align-items: center; gap: 12px; width: 100%; min-height: 52px; padding: 14px 16px; background: transparent; border: 0; color: var(--ink); font-family: inherit; font-size: 15px; font-weight: 500; text-align: left; cursor: pointer;">
|
||||
<span style="flex: 1;">{{ item.q }}</span>
|
||||
<span style="flex: none; width: 8px; height: 8px; border-right: 1.5px solid var(--ink-3); border-bottom: 1.5px solid var(--ink-3); transform: {{ item.rot }}; transition: transform 180ms ease; display: block;"></span>
|
||||
</button>
|
||||
<div hidden="{{ item.off }}" style="padding: 0 16px 16px; font-size: 14px; line-height: 1.55; color: var(--ink-2);">{{ item.a }}</div>
|
||||
</div>
|
||||
</sc-for>
|
||||
<div style="display: flex; align-items: center; gap: 12px; min-height: 52px; padding: 12px 16px; border-radius: 16px; border: 1px solid var(--border); background: var(--surface); font-size: 15px;">
|
||||
<span style="flex: 1;">Остался вопрос</span>
|
||||
<span style="color: var(--tg);">@veilix_support</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div hidden="{{ keysOff }}" style="display: grid; gap: 12px;">
|
||||
<div style="display: grid; gap: 4px; padding: 2px 2px 4px;">
|
||||
<div style="font-size: 22px; font-weight: 600; letter-spacing: -0.03em;">Ваши ключи</div>
|
||||
<div style="font-size: 13px; color: var(--ink-2);">Нажмите, чтобы открыть подключение</div>
|
||||
</div>
|
||||
<sc-for list="{{ keys }}" as="key" hint-placeholder-count="2">
|
||||
<button type="button" onClick="{{ key.open }}" style="display: grid; gap: 10px; width: 100%; min-height: 44px; padding: 16px; border-radius: 18px; border: 1px solid var(--border); background: var(--surface); color: var(--ink); font-family: inherit; text-align: left; cursor: pointer;">
|
||||
<span style="display: flex; align-items: center; gap: 8px;">
|
||||
<span style="flex: 1; font-size: 16px; font-weight: 500;">{{ key.title }}</span>
|
||||
<span style="padding: 3px 9px; border-radius: 7px; background: {{ key.badgeBg }}; color: {{ key.badgeInk }}; font-size: 11px;">{{ key.badge }}</span>
|
||||
</span>
|
||||
<span style="display: flex; flex-wrap: wrap; gap: 4px 14px; font-size: 13px; color: var(--ink-2); font-variant-numeric: tabular-nums;">
|
||||
<span>{{ key.ip }}</span>
|
||||
<span>{{ key.untilLine }}</span>
|
||||
</span>
|
||||
</button>
|
||||
</sc-for>
|
||||
<button type="button" onClick="{{ goBuy }}" style="display: flex; align-items: center; justify-content: center; gap: 8px; width: 100%; min-height: 50px; border-radius: 16px; border: 1px dashed var(--border); background: var(--surface-2); color: var(--ink-2); font-family: inherit; font-size: 15px; cursor: pointer;">Купить ещё один ключ</button>
|
||||
</div>
|
||||
|
||||
<div hidden="{{ keyOff }}" style="display: grid; gap: 14px;">
|
||||
<button type="button" onClick="{{ goKeys }}" style="justify-self: start; min-height: 44px; border: 0; background: transparent; color: var(--tg); font-family: inherit; font-size: 15px; cursor: pointer; padding: 0;">← Ключи</button>
|
||||
<div style="display: grid; gap: 4px;">
|
||||
<div style="font-size: 22px; font-weight: 600; letter-spacing: -0.03em;">{{ cur.title }}</div>
|
||||
<div style="font-size: 13px; color: var(--ink-3); font-variant-numeric: tabular-nums;">{{ cur.ip }}</div>
|
||||
</div>
|
||||
|
||||
<div style="display: grid; gap: 10px; padding: 16px; border-radius: 18px; background: var(--surface); border: 1px solid var(--border);">
|
||||
<div style="display: flex; align-items: baseline; justify-content: space-between; gap: 10px;">
|
||||
<span style="font-size: 13px; color: var(--ink-3);">Действует до</span>
|
||||
<span style="font-size: 16px; font-weight: 500;">{{ cur.until }}</span>
|
||||
</div>
|
||||
<div style="display: flex; align-items: baseline; justify-content: space-between; gap: 10px;">
|
||||
<span style="font-size: 13px; color: var(--ink-3);">Списание</span>
|
||||
<span style="font-size: 15px; font-variant-numeric: tabular-nums;">2 499 ₽ · со счёта</span>
|
||||
</div>
|
||||
<div style="font-size: 13px; color: {{ cur.leftInk }};">{{ cur.left }}</div>
|
||||
<button type="button" onClick="{{ goBilling }}" style="min-height: 44px; border-radius: 12px; border: 1px solid var(--border); background: transparent; color: var(--ink); font-family: inherit; font-size: 15px; cursor: pointer;">Счёт и платежи</button>
|
||||
</div>
|
||||
|
||||
<div style="display: grid; gap: 10px; padding: 16px; border-radius: 18px; background: var(--surface); border: 1px solid var(--border);">
|
||||
<div style="display: flex; align-items: baseline; justify-content: space-between; gap: 10px;">
|
||||
<span style="font-size: 16px; font-weight: 500;">WireGuard</span>
|
||||
<span style="font-size: 12px; color: var(--ink-3);">5 устройств</span>
|
||||
</div>
|
||||
<sc-for list="{{ configs }}" as="cfg" hint-placeholder-count="5">
|
||||
<div style="display: flex; align-items: center; gap: 10px; padding: 10px 12px; border-radius: 14px; background: var(--surface-2);">
|
||||
<span style="flex: 1; min-width: 0; display: grid; gap: 2px;">
|
||||
<span style="font-size: 15px;">{{ cfg.name }}</span>
|
||||
<span style="font-size: 12px; color: var(--ink-3);">{{ cfg.state }}</span>
|
||||
</span>
|
||||
<button type="button" onClick="{{ cfg.showQr }}" style="min-height: 44px; padding: 0 14px; border-radius: 11px; border: 0; background: var(--accent); color: var(--on-accent); font-family: inherit; font-size: 14px; cursor: pointer;">QR</button>
|
||||
<button type="button" style="min-height: 44px; padding: 0 14px; border-radius: 11px; border: 1px solid var(--border); background: var(--bg); color: var(--ink); font-family: inherit; font-size: 14px; cursor: pointer;">Файл</button>
|
||||
</div>
|
||||
</sc-for>
|
||||
</div>
|
||||
|
||||
<div style="display: grid; gap: 10px; padding: 16px; border-radius: 18px; background: var(--surface); border: 1px solid var(--border);">
|
||||
<div style="display: flex; align-items: baseline; justify-content: space-between; gap: 10px;">
|
||||
<span style="font-size: 16px; font-weight: 500;">Xray</span>
|
||||
<span style="font-size: 12px; color: var(--ink-3);">Happ и другие</span>
|
||||
</div>
|
||||
<div style="padding: 12px; border-radius: 12px; background: var(--surface-2); font-size: 12px; color: var(--ink-2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">{{ cur.xray }}</div>
|
||||
<div style="display: flex; gap: 8px;">
|
||||
<button type="button" onClick="{{ copyXray }}" style="flex: 1; min-height: 46px; border-radius: 12px; border: 0; background: var(--accent); color: var(--on-accent); font-family: inherit; font-size: 15px; cursor: pointer;">{{ copyLabel }}</button>
|
||||
<button type="button" onClick="{{ showXrayQr }}" style="min-height: 46px; padding: 0 18px; border-radius: 12px; border: 1px solid var(--border); background: transparent; color: var(--ink); font-family: inherit; font-size: 15px; cursor: pointer;">QR</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div hidden="{{ moreOff }}" style="display: grid; gap: 12px;">
|
||||
<div style="font-size: 22px; font-weight: 600; letter-spacing: -0.03em; padding: 2px;">Ещё</div>
|
||||
<div style="display: grid; gap: 1px; border-radius: 18px; overflow: hidden; border: 1px solid var(--border); background: var(--border);">
|
||||
<button type="button" onClick="{{ goBilling }}" style="display: flex; align-items: center; gap: 12px; min-height: 54px; padding: 12px 16px; border: 0; background: var(--surface); color: var(--ink); font-family: inherit; font-size: 15px; text-align: left; cursor: pointer;">
|
||||
<span style="flex: 1;">Счёт и платежи</span>
|
||||
<span style="color: var(--ink-3); font-variant-numeric: tabular-nums;">1 250 ₽</span>
|
||||
<span style="color: var(--ink-3);">›</span>
|
||||
</button>
|
||||
<button type="button" onClick="{{ goRef }}" style="display: flex; align-items: center; gap: 12px; min-height: 54px; padding: 12px 16px; border: 0; background: var(--surface); color: var(--ink); font-family: inherit; font-size: 15px; text-align: left; cursor: pointer;">
|
||||
<span style="flex: 1;">Реферальная программа</span>
|
||||
<span style="color: var(--ink-3);">{{ refStats }}</span>
|
||||
<span style="color: var(--ink-3);">›</span>
|
||||
</button>
|
||||
<button type="button" onClick="{{ goLead }}" style="display: flex; align-items: center; gap: 12px; min-height: 54px; padding: 12px 16px; border: 0; background: var(--surface); color: var(--ink); font-family: inherit; font-size: 15px; text-align: left; cursor: pointer;">
|
||||
<span style="flex: 1;">Индивидуальные условия</span>
|
||||
<span style="color: var(--ink-3);">›</span>
|
||||
</button>
|
||||
<button type="button" onClick="{{ goFaq }}" style="display: flex; align-items: center; gap: 12px; min-height: 54px; padding: 12px 16px; border: 0; background: var(--surface); color: var(--ink); font-family: inherit; font-size: 15px; text-align: left; cursor: pointer;">
|
||||
<span style="flex: 1;">Частые вопросы</span>
|
||||
<span style="color: var(--ink-3);">›</span>
|
||||
</button>
|
||||
</div>
|
||||
<div style="display: grid; gap: 1px; border-radius: 18px; overflow: hidden; border: 1px solid var(--border); background: var(--border);">
|
||||
<div style="display: flex; align-items: center; gap: 12px; min-height: 52px; padding: 12px 16px; background: var(--surface); font-size: 15px;">
|
||||
<span style="flex: 1;">Написать в поддержку</span>
|
||||
<span style="color: var(--tg);">@veilix_support</span>
|
||||
</div>
|
||||
<div style="display: flex; align-items: center; gap: 12px; min-height: 52px; padding: 12px 16px; background: var(--surface); font-size: 15px;">
|
||||
<span style="flex: 1;">Почта аккаунта</span>
|
||||
<span style="color: var(--ink-3);">anna@example.com</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: grid; gap: 12px; padding: 14px 16px; border-radius: 18px; background: var(--surface); border: 1px solid var(--border);">
|
||||
<div style="display: flex; align-items: center; justify-content: space-between; gap: 12px;">
|
||||
<span style="font-size: 15px;">Тема</span>
|
||||
<span style="display: flex; flex: none; padding: 3px; border-radius: 11px; border: 1px solid var(--border);">
|
||||
<button type="button" onClick="{{ setLight }}" title="Светлая тема" style="display: flex; align-items: center; justify-content: center; width: 40px; height: 34px; border: 0; border-radius: 8px; background: {{ lightBg }}; color: var(--ink); cursor: pointer;">
|
||||
<span style="position: relative; width: 16px; height: 16px; display: block;">
|
||||
<span style="position: absolute; left: 4px; top: 4px; width: 8px; height: 8px; border-radius: 50%; background: currentColor; display: block;"></span>
|
||||
<span style="position: absolute; left: 0; top: 0; width: 16px; height: 16px; border-radius: 50%; border: 1.5px solid currentColor; opacity: 0.45; display: block;"></span>
|
||||
</span>
|
||||
</button>
|
||||
<button type="button" onClick="{{ setDark }}" title="Тёмная тема" style="display: flex; align-items: center; justify-content: center; width: 40px; height: 34px; border: 0; border-radius: 8px; background: {{ darkBg }}; color: var(--ink); cursor: pointer;">
|
||||
<span style="width: 16px; height: 16px; border-radius: 50%; box-shadow: inset -5px -2px 0 0 currentColor; display: block;"></span>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
<div style="display: flex; align-items: center; justify-content: space-between; gap: 12px; padding-top: 12px; border-top: 1px solid var(--border);">
|
||||
<span style="font-size: 15px;">Язык</span>
|
||||
<span style="display: flex; flex: none; padding: 3px; border-radius: 11px; border: 1px solid var(--border);">
|
||||
<button type="button" onClick="{{ setRu }}" style="height: 34px; padding: 0 16px; border: 0; border-radius: 8px; background: {{ ruBg }}; color: {{ ruInk }}; font-family: inherit; font-size: 14px; cursor: pointer;">RU</button>
|
||||
<button type="button" onClick="{{ setEn }}" style="height: 34px; padding: 0 16px; border: 0; border-radius: 8px; background: {{ enBg }}; color: {{ enInk }}; font-family: inherit; font-size: 14px; cursor: pointer;">EN</button>
|
||||
</span>
|
||||
</div>
|
||||
<div style="font-size: 12px; color: var(--ink-3); line-height: 1.45;">По умолчанию тема подстраивается под Telegram.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div hidden="{{ billingOff }}" style="display: grid; gap: 12px;">
|
||||
<button type="button" onClick="{{ goMore }}" style="justify-self: start; min-height: 44px; border: 0; background: transparent; color: var(--tg); font-family: inherit; font-size: 15px; cursor: pointer; padding: 0;">← Ещё</button>
|
||||
<div style="display: grid; gap: 4px; padding: 0 2px 2px;">
|
||||
<div style="font-size: 22px; font-weight: 600; letter-spacing: -0.03em;">Счёт и платежи</div>
|
||||
<div style="font-size: 13px; line-height: 1.5; color: var(--ink-2);">Карта привязана к аккаунту, а не к отдельному ключу. Списания за все ключи идут с неё или с баланса.</div>
|
||||
</div>
|
||||
|
||||
<div style="display: grid; gap: 12px; padding: 16px; border-radius: 18px; background: var(--surface); border: 1px solid var(--border);">
|
||||
<div style="display: grid; gap: 3px;">
|
||||
<span style="font-size: 13px; color: var(--ink-3);">Баланс</span>
|
||||
<span style="font-size: 28px; font-weight: 600; letter-spacing: -0.03em; font-variant-numeric: tabular-nums;">1 250 ₽</span>
|
||||
</div>
|
||||
<button type="button" onClick="{{ openTopup }}" style="min-height: 48px; border-radius: 12px; border: 0; background: var(--accent); color: var(--on-accent); font-family: inherit; font-size: 15px; font-weight: 500; cursor: pointer;">Пополнить баланс</button>
|
||||
</div>
|
||||
|
||||
<div hidden="{{ topupOff }}" style="display: grid; gap: 12px; padding: 16px; border-radius: 18px; background: var(--surface); border: 1px solid var(--accent);">
|
||||
<div style="display: flex; align-items: baseline; justify-content: space-between; gap: 10px;">
|
||||
<span style="font-size: 16px; font-weight: 500;">Пополнение</span>
|
||||
<button type="button" onClick="{{ closeTopup }}" style="border: 0; background: transparent; color: var(--ink-3); font-family: inherit; font-size: 14px; cursor: pointer; padding: 0;">Свернуть</button>
|
||||
</div>
|
||||
<div style="display: flex; flex-wrap: wrap; gap: 8px;">
|
||||
<sc-for list="{{ sums }}" as="sum" hint-placeholder-count="3">
|
||||
<button type="button" onClick="{{ sum.pick }}" style="min-height: 44px; padding: 0 16px; border-radius: 12px; border: 1px solid {{ sum.border }}; background: {{ sum.bg }}; color: var(--ink); font-family: inherit; font-size: 15px; font-variant-numeric: tabular-nums; cursor: pointer;">{{ sum.label }}</button>
|
||||
</sc-for>
|
||||
</div>
|
||||
<div style="display: grid; gap: 6px;">
|
||||
<sc-for list="{{ methods }}" as="m" hint-placeholder-count="3">
|
||||
<button type="button" onClick="{{ m.pick }}" style="display: flex; align-items: center; gap: 10px; min-height: 48px; padding: 0 14px; border-radius: 12px; border: 1px solid {{ m.border }}; background: {{ m.bg }}; color: var(--ink); font-family: inherit; font-size: 15px; text-align: left; cursor: pointer;">
|
||||
<span style="flex: 1;">{{ m.label }}</span>
|
||||
<span style="font-size: 12px; color: var(--ink-3);">{{ m.note }}</span>
|
||||
</button>
|
||||
</sc-for>
|
||||
</div>
|
||||
<div hidden="{{ cryptoOff }}" style="padding: 12px 14px; border-radius: 12px; background: var(--surface-2); font-size: 13px; line-height: 1.5; color: var(--ink-2);">USDT (TRC-20), BTC, ETH. Покажем адрес и сумму в монете, курс фиксируется на 15 минут. Баланс пополнится после одного подтверждения сети.</div>
|
||||
<button type="button" onClick="{{ sendTopup }}" style="min-height: 48px; border-radius: 12px; border: 0; background: var(--accent); color: var(--on-accent); font-family: inherit; font-size: 15px; font-weight: 500; cursor: pointer;">{{ topupButton }}</button>
|
||||
</div>
|
||||
|
||||
<div style="display: grid; gap: 1px; border-radius: 18px; overflow: hidden; border: 1px solid var(--border); background: var(--border);">
|
||||
<div style="display: flex; align-items: center; gap: 12px; min-height: 52px; padding: 12px 16px; background: var(--surface); font-size: 15px;">
|
||||
<span style="flex: 1;">Карта аккаунта</span>
|
||||
<span style="color: var(--ink-3); font-variant-numeric: tabular-nums;">···· 4242</span>
|
||||
</div>
|
||||
<div style="display: flex; align-items: center; gap: 12px; min-height: 52px; padding: 12px 16px; background: var(--surface); font-size: 15px;">
|
||||
<span style="flex: 1;">Следующее списание</span>
|
||||
<span style="color: var(--ink-3); font-variant-numeric: tabular-nums;">2 499 ₽ · 21 сент.</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="font-size: 16px; font-weight: 500; padding: 8px 2px 0;">История платежей</div>
|
||||
<sc-for list="{{ payments }}" as="p" hint-placeholder-count="5">
|
||||
<div style="display: flex; align-items: center; gap: 12px; padding: 14px 16px; border-radius: 16px; background: var(--surface); border: 1px solid var(--border);">
|
||||
<span style="flex: 1; min-width: 0; display: grid; gap: 3px;">
|
||||
<span style="font-size: 14px;">{{ p.title }}</span>
|
||||
<span style="font-size: 12px; color: var(--ink-3);">{{ p.meta }}</span>
|
||||
</span>
|
||||
<span style="flex: none; font-size: 15px; font-weight: 500; color: {{ p.ink }}; font-variant-numeric: tabular-nums;">{{ p.amount }}</span>
|
||||
</div>
|
||||
</sc-for>
|
||||
</div>
|
||||
|
||||
<div hidden="{{ refOff }}" style="display: grid; gap: 12px;">
|
||||
<button type="button" onClick="{{ goMore }}" style="justify-self: start; min-height: 44px; border: 0; background: transparent; color: var(--tg); font-family: inherit; font-size: 15px; cursor: pointer; padding: 0;">← Ещё</button>
|
||||
<div style="display: grid; gap: 4px; padding: 0 2px 2px;">
|
||||
<div style="font-size: 22px; font-weight: 600; letter-spacing: -0.03em;">Реферальная программа</div>
|
||||
<div style="font-size: 13px; line-height: 1.5; color: var(--ink-2);">Приведите друга — вы оба получите по месяцу в подарок.</div>
|
||||
</div>
|
||||
<div style="display: flex; gap: 8px;">
|
||||
<div style="flex: 1; display: grid; gap: 4px; padding: 14px; border-radius: 16px; background: var(--surface); border: 1px solid var(--border);">
|
||||
<span style="font-size: 12px; color: var(--ink-3);">Приглашено</span>
|
||||
<span style="font-size: 24px; font-weight: 600; letter-spacing: -0.03em; font-variant-numeric: tabular-nums;">3</span>
|
||||
</div>
|
||||
<div style="flex: 1; display: grid; gap: 4px; padding: 14px; border-radius: 16px; background: var(--surface); border: 1px solid var(--border);">
|
||||
<span style="font-size: 12px; color: var(--ink-3);">Месяцев</span>
|
||||
<span style="font-size: 24px; font-weight: 600; letter-spacing: -0.03em; font-variant-numeric: tabular-nums;">2</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: grid; gap: 10px; padding: 16px; border-radius: 18px; background: var(--surface); border: 1px solid var(--border);">
|
||||
<div style="font-size: 15px; font-weight: 500;">Ваша ссылка</div>
|
||||
<div style="padding: 12px; border-radius: 12px; background: var(--surface-2); font-size: 13px; color: var(--ink-2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">t.me/veilix_bot?start=anna7f2c</div>
|
||||
<div style="display: flex; gap: 8px;">
|
||||
<button type="button" onClick="{{ copyRef }}" style="flex: 1; min-height: 46px; border-radius: 12px; border: 0; background: var(--accent); color: var(--on-accent); font-family: inherit; font-size: 15px; cursor: pointer;">{{ refLabel }}</button>
|
||||
<button type="button" style="min-height: 46px; padding: 0 18px; border-radius: 12px; border: 1px solid var(--border); background: transparent; color: var(--ink); font-family: inherit; font-size: 15px; cursor: pointer;">Поделиться</button>
|
||||
</div>
|
||||
<div style="font-size: 12px; line-height: 1.45; color: var(--ink-3);">Месяц начисляется после первой оплаты друга и применяется к ближайшему списанию.</div>
|
||||
</div>
|
||||
<div style="display: grid; gap: 1px; border-radius: 18px; overflow: hidden; border: 1px solid var(--border); background: var(--border);">
|
||||
<div style="display: flex; align-items: center; gap: 12px; min-height: 50px; padding: 12px 16px; background: var(--surface); font-size: 14px;">
|
||||
<span style="flex: 1;">m···@gmail.com</span>
|
||||
<span style="color: var(--accent);">Месяц начислен</span>
|
||||
</div>
|
||||
<div style="display: flex; align-items: center; gap: 12px; min-height: 50px; padding: 12px 16px; background: var(--surface); font-size: 14px;">
|
||||
<span style="flex: 1;">d···@yandex.ru</span>
|
||||
<span style="color: var(--accent);">Месяц начислен</span>
|
||||
</div>
|
||||
<div style="display: flex; align-items: center; gap: 12px; min-height: 50px; padding: 12px 16px; background: var(--surface); font-size: 14px;">
|
||||
<span style="flex: 1;">k···@icloud.com</span>
|
||||
<span style="color: var(--ink-3);">Ждём оплаты</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div hidden="{{ leadOff }}" style="display: grid; gap: 12px;">
|
||||
<button type="button" onClick="{{ goMore }}" style="justify-self: start; min-height: 44px; border: 0; background: transparent; color: var(--tg); font-family: inherit; font-size: 15px; cursor: pointer; padding: 0;">← Ещё</button>
|
||||
<div style="display: grid; gap: 4px; padding: 0 2px 2px;">
|
||||
<div style="font-size: 22px; font-weight: 600; letter-spacing: -0.03em;">Индивидуальные условия</div>
|
||||
<div style="font-size: 13px; line-height: 1.5; color: var(--ink-2);">Команда, несколько адресов, редкая страна или свой сценарий маршрутизации. Ответим в течение дня.</div>
|
||||
</div>
|
||||
<div style="display: grid; gap: 10px; padding: 16px; border-radius: 18px; background: var(--surface); border: 1px solid var(--border);">
|
||||
<input type="text" placeholder="Имя" style="min-height: 46px; padding: 0 14px; border-radius: 12px; border: 1px solid var(--border); background: var(--bg); color: var(--ink); font-family: inherit; width: 100%;" />
|
||||
<input type="text" placeholder="Telegram или почта" style="min-height: 46px; padding: 0 14px; border-radius: 12px; border: 1px solid var(--border); background: var(--bg); color: var(--ink); font-family: inherit; width: 100%;" />
|
||||
<input type="text" placeholder="Сколько адресов и в каких странах" style="min-height: 46px; padding: 0 14px; border-radius: 12px; border: 1px solid var(--border); background: var(--bg); color: var(--ink); font-family: inherit; width: 100%;" />
|
||||
<textarea rows="4" placeholder="Коротко о задаче" style="padding: 12px 14px; border-radius: 12px; border: 1px solid var(--border); background: var(--bg); color: var(--ink); font-family: inherit; resize: vertical; width: 100%;"></textarea>
|
||||
<button type="button" onClick="{{ sendLead }}" style="min-height: 48px; border-radius: 12px; border: 0; background: var(--accent); color: var(--on-accent); font-family: inherit; font-size: 15px; font-weight: 500; cursor: pointer;">{{ leadButton }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="position: absolute; left: 50%; bottom: 46px; transform: translateX(-50%); z-index: 15; display: flex; gap: 2px; padding: 6px; border-radius: 999px; background: color-mix(in oklab, var(--surface) 62%, transparent); backdrop-filter: blur(24px) saturate(1.8); -webkit-backdrop-filter: blur(24px) saturate(1.8); border: 1px solid color-mix(in oklab, var(--ink) 8%, transparent); box-shadow: 0 10px 30px -10px color-mix(in oklab, var(--ink) 40%, transparent);">
|
||||
<button type="button" onClick="{{ goBuy }}" style="display: grid; justify-items: center; gap: 4px; min-width: 72px; min-height: 52px; padding: 8px 10px; border: 0; border-radius: 999px; background: {{ tabBuyBg }}; color: {{ tabBuyInk }}; font-family: inherit; font-size: 11px; cursor: pointer;">
|
||||
<span style="position: relative; width: 20px; height: 20px; display: block;">
|
||||
<span style="position: absolute; left: 8px; top: 0; width: 4px; height: 20px; border-radius: 2px; background: currentColor; display: block;"></span>
|
||||
<span style="position: absolute; left: 0; top: 8px; width: 20px; height: 4px; border-radius: 2px; background: currentColor; display: block;"></span>
|
||||
</span>
|
||||
Купить
|
||||
</button>
|
||||
<button type="button" onClick="{{ goFaq }}" style="display: grid; justify-items: center; gap: 4px; min-width: 72px; min-height: 52px; padding: 8px 10px; border: 0; border-radius: 999px; background: {{ tabFaqBg }}; color: {{ tabFaqInk }}; font-family: inherit; font-size: 11px; cursor: pointer;">
|
||||
<span style="display: flex; align-items: center; justify-content: center; width: 20px; height: 20px; border-radius: 50%; border: 2px solid currentColor; font-size: 13px; font-weight: 600; line-height: 1;">?</span>
|
||||
FAQ
|
||||
</button>
|
||||
<button type="button" onClick="{{ goKeys }}" style="display: grid; justify-items: center; gap: 4px; min-width: 72px; min-height: 52px; padding: 8px 10px; border: 0; border-radius: 999px; background: {{ tabKeysBg }}; color: {{ tabKeysInk }}; font-family: inherit; font-size: 11px; cursor: pointer;">
|
||||
<span style="position: relative; width: 20px; height: 20px; display: block;">
|
||||
<span style="position: absolute; left: 0; top: 2px; width: 20px; height: 4px; border-radius: 2px; background: currentColor; display: block;"></span>
|
||||
<span style="position: absolute; left: 0; top: 9px; width: 20px; height: 4px; border-radius: 2px; background: currentColor; opacity: 0.6; display: block;"></span>
|
||||
<span style="position: absolute; left: 0; top: 16px; width: 20px; height: 4px; border-radius: 2px; background: currentColor; opacity: 0.3; display: block;"></span>
|
||||
</span>
|
||||
Ключи
|
||||
</button>
|
||||
<button type="button" onClick="{{ goMore }}" style="display: grid; justify-items: center; gap: 4px; min-width: 72px; min-height: 52px; padding: 8px 10px; border: 0; border-radius: 999px; background: {{ tabMoreBg }}; color: {{ tabMoreInk }}; font-family: inherit; font-size: 11px; cursor: pointer;">
|
||||
<span style="position: relative; width: 20px; height: 20px; display: block;">
|
||||
<span style="position: absolute; left: 0; top: 8px; width: 4px; height: 4px; border-radius: 50%; background: currentColor; display: block;"></span>
|
||||
<span style="position: absolute; left: 8px; top: 8px; width: 4px; height: 4px; border-radius: 50%; background: currentColor; display: block;"></span>
|
||||
<span style="position: absolute; left: 16px; top: 8px; width: 4px; height: 4px; border-radius: 50%; background: currentColor; display: block;"></span>
|
||||
</span>
|
||||
Ещё
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div hidden="{{ payOff }}" onClick="{{ closePay }}" style="position: absolute; left: 0; right: 0; top: -62px; bottom: -34px; z-index: 22; display: flex; align-items: flex-end; justify-content: center; padding-bottom: 34px; background: color-mix(in oklab, var(--ink) 45%, transparent);">
|
||||
<div onClick="{{ stopPay }}" style="width: 100%; display: grid; gap: 14px; padding: 22px 20px 34px; border-radius: 22px 22px 0 0; background: var(--surface); border-top: 1px solid var(--border);">
|
||||
<span style="justify-self: center; width: 36px; height: 4px; border-radius: 2px; background: var(--border); display: block;"></span>
|
||||
<div style="display: flex; align-items: center; gap: 12px;">
|
||||
{{ payFlag }}
|
||||
<span style="flex: 1; min-width: 0; display: grid; gap: 2px;">
|
||||
<span style="font-size: 17px; font-weight: 600; letter-spacing: -0.02em;">{{ payTitle }}</span>
|
||||
<span style="font-size: 12px; color: var(--accent);">{{ paySub }}</span>
|
||||
</span>
|
||||
</div>
|
||||
<div style="display: grid; gap: 8px; padding: 14px 16px; border-radius: 14px; background: var(--surface-2);">
|
||||
<div style="display: flex; align-items: baseline; justify-content: space-between; gap: 10px; font-size: 14px;">
|
||||
<span style="color: var(--ink-2);">Первый месяц</span>
|
||||
<span style="font-size: 17px; font-weight: 600; font-variant-numeric: tabular-nums;">{{ paySetup }}</span>
|
||||
</div>
|
||||
<div style="display: flex; align-items: baseline; justify-content: space-between; gap: 10px; font-size: 14px;">
|
||||
<span style="color: var(--ink-2);">Далее каждый месяц</span>
|
||||
<span style="font-variant-numeric: tabular-nums;">{{ payMonth }}</span>
|
||||
</div>
|
||||
<div style="display: flex; align-items: baseline; justify-content: space-between; gap: 10px; font-size: 14px; padding-top: 8px; border-top: 1px solid var(--border);">
|
||||
<span style="color: var(--ink-2);">В комплекте</span>
|
||||
<span>RU-адрес · 5 устройств</span>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" onClick="{{ confirmPay }}" style="min-height: 52px; border-radius: 14px; border: 0; background: var(--accent); color: var(--on-accent); font-family: inherit; font-size: 16px; font-weight: 500; cursor: pointer;">{{ payButton }}</button>
|
||||
<div style="font-size: 12px; line-height: 1.45; color: var(--ink-3); text-align: center;">Оплата пройдёт внутри Telegram. Ключ появится во вкладке «Ключи» сразу после подтверждения.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div hidden="{{ qrOff }}" onClick="{{ closeQr }}" style="position: absolute; left: 0; right: 0; top: -62px; bottom: -34px; z-index: 20; display: flex; align-items: flex-end; justify-content: center; padding-bottom: 34px; background: color-mix(in oklab, var(--ink) 45%, transparent);">
|
||||
<div style="width: 100%; display: grid; gap: 14px; justify-items: center; padding: 22px 20px 34px; border-radius: 22px 22px 0 0; background: var(--surface); border-top: 1px solid var(--border);">
|
||||
<span style="width: 36px; height: 4px; border-radius: 2px; background: var(--border); display: block;"></span>
|
||||
<span style="font-size: 16px; font-weight: 500;">{{ qrTitle }}</span>
|
||||
{{ qrImage }}
|
||||
<span style="font-size: 12px; color: var(--ink-3); text-align: center; max-width: 240px; line-height: 1.45;">Наведите камеру телефона на код в приложении WireGuard или Happ.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</x-import>
|
||||
|
||||
<div style="max-width: 320px; display: grid; gap: 14px; align-content: start; padding-top: 12px;">
|
||||
<div style="font-size: 20px; font-weight: 600; letter-spacing: -0.03em; color: var(--ink);">Мини-приложение в Telegram</div>
|
||||
<p style="margin: 0; font-size: 15px; line-height: 1.55; color: var(--ink-2); font-weight: 300;">В Telegram приложение работает и как витрина, и как кабинет: человек может попасть сюда, ни разу не открыв сайт. Поэтому первая вкладка — «Купить»: тезисы, все локации с ценами и кнопкой покупки у каждой, форма на индивидуальные условия внизу.</p>
|
||||
<p style="margin: 0; font-size: 14px; line-height: 1.55; color: var(--ink-3); font-weight: 300;">Вкладки: Купить, FAQ, Ключи, Ещё. В «Ещё» — счёт и платежи, реферальная программа, индивидуальный заказ, тема и язык.</p>
|
||||
</div>
|
||||
</div>
|
||||
</x-dc>
|
||||
<script type="text/x-dc" data-dc-script data-props="{"defaultTheme":{"editor":"enum","options":["light","dark","system"],"default":"light","tsType":"'light' | 'dark' | 'system'","section":"Тема"}}">
|
||||
class Component extends DCLogic {
|
||||
state = { theme: null, sysDark: false, view: "buy", key: 0, qr: null, copied: false, lang: "ru", refCopied: false, faq: -1, leadSent: false, topup: false, sum: 1, method: 0, topupSent: false, pay: null, paid: false };
|
||||
|
||||
componentDidMount() {
|
||||
const mq = window.matchMedia("(prefers-color-scheme: dark)");
|
||||
this.mq = mq;
|
||||
this.onSys = () => this.setState({ sysDark: mq.matches });
|
||||
mq.addEventListener("change", this.onSys);
|
||||
let saved = null;
|
||||
try { saved = localStorage.getItem("veilix.theme"); } catch (e) {}
|
||||
let savedLang = null;
|
||||
try { savedLang = localStorage.getItem("veilix.lang"); } catch (e) {}
|
||||
this.setState({ sysDark: mq.matches, theme: saved || this.props.defaultTheme || "light", lang: savedLang || "ru" }, () => { this.apply(); this.applyLang(); });
|
||||
}
|
||||
|
||||
applyLang() {
|
||||
const lang = this.state.lang || "ru";
|
||||
if (window.VEILIX_APPLY_LANG) window.VEILIX_APPLY_LANG(document.body, lang);
|
||||
else setTimeout(() => this.applyLang(), 150);
|
||||
}
|
||||
|
||||
setLang(l) {
|
||||
try { localStorage.setItem("veilix.lang", l); } catch (e) {}
|
||||
this.setState({ lang: l }, () => this.applyLang());
|
||||
}
|
||||
|
||||
componentWillUnmount() { if (this.mq) this.mq.removeEventListener("change", this.onSys); }
|
||||
componentDidUpdate() { this.apply(); this.applyLang(); }
|
||||
|
||||
apply() {
|
||||
const theme = this.state.theme || this.props.defaultTheme || "light";
|
||||
const dark = theme === "dark" || (theme === "system" && this.state.sysDark);
|
||||
document.documentElement.setAttribute("data-veilix-theme", dark ? "dark" : "light");
|
||||
}
|
||||
|
||||
setTheme(t) {
|
||||
try { localStorage.setItem("veilix.theme", t); } catch (e) {}
|
||||
this.setState({ theme: t }, () => this.apply());
|
||||
}
|
||||
|
||||
data() {
|
||||
const base = { setup: "4 900 ₽", month: "2 499 ₽" };
|
||||
const rare = { setup: "6 900 ₽", month: "3 499 ₽" };
|
||||
const far = { setup: "9 900 ₽", month: "4 999 ₽" };
|
||||
const L = (country, city, code, servers, tier) => ({ country, city, code, servers, setup: tier.setup, month: tier.month });
|
||||
return [
|
||||
L("Нидерланды", "Амстердам", "nl", 12, base),
|
||||
L("Германия", "Франкфурт", "de", 10, base),
|
||||
L("США", "Нью-Йорк", "us", 9, base),
|
||||
L("Великобритания", "Лондон", "gb", 8, base),
|
||||
L("Франция", "Париж", "fr", 7, base),
|
||||
L("Финляндия", "Хельсинки", "fi", 6, base),
|
||||
L("Швеция", "Стокгольм", "se", 5, base),
|
||||
L("Норвегия", "Осло", "no", 0, base),
|
||||
L("Дания", "Копенгаген", "dk", 0, base),
|
||||
L("Швейцария", "Цюрих", "ch", 4, rare),
|
||||
L("Австрия", "Вена", "at", 5, base),
|
||||
L("Испания", "Мадрид", "es", 6, base),
|
||||
L("Италия", "Милан", "it", 6, base),
|
||||
L("Польша", "Варшава", "pl", 7, base),
|
||||
L("Чехия", "Прага", "cz", 5, base),
|
||||
L("Румыния", "Бухарест", "ro", 0, base),
|
||||
L("Литва", "Вильнюс", "lt", 4, base),
|
||||
L("Латвия", "Рига", "lv", 4, base),
|
||||
L("Эстония", "Таллин", "ee", 0, base),
|
||||
L("Турция", "Стамбул", "tr", 6, base),
|
||||
L("Казахстан", "Алматы", "kz", 0, base),
|
||||
L("Армения", "Ереван", "am", 0, base),
|
||||
L("Грузия", "Тбилиси", "ge", 0, base),
|
||||
L("ОАЭ", "Дубай", "ae", 3, rare),
|
||||
L("Сингапур", "Сингапур", "sg", 4, rare),
|
||||
L("Япония", "Токио", "jp", 4, rare),
|
||||
L("Южная Корея", "Сеул", "kr", 0, rare),
|
||||
L("Канада", "Торонто", "ca", 6, base),
|
||||
L("Австралия", "Сидней", "au", 0, far),
|
||||
L("Бразилия", "Сан-Паулу", "br", 0, far)
|
||||
];
|
||||
}
|
||||
|
||||
flagSpecs() {
|
||||
return {
|
||||
nl: { rows: ["#AE1C28", "#FFFFFF", "#21468B"] },
|
||||
de: { rows: ["#000000", "#DD0000", "#FFCE00"] },
|
||||
us: { rows: ["#B22234", "#FFFFFF", "#B22234", "#FFFFFF", "#B22234", "#FFFFFF"], marks: [{ l: 0, t: 0, w: 19, h: 15, bg: "#3C3B6E" }] },
|
||||
gb: { bg: "#012169", marks: [{ l: 15, t: 0, w: 10, h: 28, bg: "#FFFFFF" }, { l: 0, t: 9, w: 40, h: 10, bg: "#FFFFFF" }, { l: 17, t: 0, w: 5, h: 28, bg: "#C8102E" }, { l: 0, t: 11, w: 40, h: 5, bg: "#C8102E" }] },
|
||||
fr: { cols: ["#002395", "#FFFFFF", "#ED2939"] },
|
||||
fi: { bg: "#FFFFFF", marks: [{ l: 11, t: 0, w: 7, h: 28, bg: "#003580" }, { l: 0, t: 10, w: 40, h: 7, bg: "#003580" }] },
|
||||
se: { bg: "#006AA7", marks: [{ l: 11, t: 0, w: 5, h: 28, bg: "#FECC00" }, { l: 0, t: 11, w: 40, h: 5, bg: "#FECC00" }] },
|
||||
no: { bg: "#BA0C2F", marks: [{ l: 10, t: 0, w: 9, h: 28, bg: "#FFFFFF" }, { l: 0, t: 9, w: 40, h: 9, bg: "#FFFFFF" }, { l: 13, t: 0, w: 3, h: 28, bg: "#00205B" }, { l: 0, t: 12, w: 40, h: 3, bg: "#00205B" }] },
|
||||
dk: { bg: "#C8102E", marks: [{ l: 11, t: 0, w: 5, h: 28, bg: "#FFFFFF" }, { l: 0, t: 11, w: 40, h: 5, bg: "#FFFFFF" }] },
|
||||
ch: { bg: "#D52B1E", marks: [{ l: 17, t: 7, w: 5, h: 14, bg: "#FFFFFF" }, { l: 13, t: 11, w: 14, h: 5, bg: "#FFFFFF" }] },
|
||||
at: { rows: ["#ED2939", "#FFFFFF", "#ED2939"] },
|
||||
es: { rows: ["#AA151B", "#F1BF00", "#AA151B"], sizes: "1fr 2fr 1fr" },
|
||||
it: { cols: ["#008C45", "#F4F5F0", "#CD212A"] },
|
||||
pl: { rows: ["#FFFFFF", "#DC143C"] },
|
||||
cz: { rows: ["#FFFFFF", "#D7141A"], marks: [{ l: 0, t: 0, w: 16, h: 28, bg: "#11457E", clip: "polygon(0 0, 100% 50%, 0 100%)" }] },
|
||||
ro: { cols: ["#002B7F", "#FCD116", "#CE1126"] },
|
||||
lt: { rows: ["#FDB913", "#006A44", "#C1272D"] },
|
||||
lv: { rows: ["#9E3039", "#FFFFFF", "#9E3039"], sizes: "2fr 1fr 2fr" },
|
||||
ee: { rows: ["#0072CE", "#000000", "#FFFFFF"] },
|
||||
tr: { bg: "#E30A17", marks: [{ l: 9, t: 7, w: 14, h: 14, r: "50%", bg: "#FFFFFF" }, { l: 13, t: 7, w: 14, h: 14, r: "50%", bg: "#E30A17" }] },
|
||||
kz: { bg: "#00AFCA", marks: [{ l: 14, t: 7, w: 13, h: 13, r: "50%", bg: "#FEC50C" }] },
|
||||
am: { rows: ["#D90012", "#0033A0", "#F2A800"] },
|
||||
ge: { bg: "#FFFFFF", marks: [{ l: 17, t: 0, w: 5, h: 28, bg: "#FF0000" }, { l: 0, t: 11, w: 40, h: 5, bg: "#FF0000" }] },
|
||||
ae: { rows: ["#00732F", "#FFFFFF", "#000000"], marks: [{ l: 0, t: 0, w: 10, h: 28, bg: "#FF0000" }] },
|
||||
sg: { rows: ["#EF3340", "#FFFFFF"], marks: [{ l: 4, t: 3, w: 10, h: 10, r: "50%", bg: "#FFFFFF" }, { l: 8, t: 3, w: 10, h: 10, r: "50%", bg: "#EF3340" }] },
|
||||
jp: { bg: "#FFFFFF", marks: [{ l: 14, t: 8, w: 12, h: 12, r: "50%", bg: "#BC002D" }] },
|
||||
kr: { bg: "#FFFFFF", marks: [{ l: 14, t: 8, w: 12, h: 12, r: "50%", bg: "#CD2E3A" }, { l: 14, t: 8, w: 12, h: 12, r: "50%", bg: "#0047A0", clip: "polygon(0 50%, 100% 50%, 100% 100%, 0 100%)" }] },
|
||||
ca: { cols: ["#D80621", "#FFFFFF", "#D80621"], sizes: "1fr 2fr 1fr", marks: [{ l: 17, t: 8, w: 6, h: 12, bg: "#D80621", clip: "polygon(50% 0, 100% 100%, 0 100%)" }] },
|
||||
au: { bg: "#00247D", marks: [{ l: 8, t: 0, w: 3, h: 14, bg: "#FFFFFF" }, { l: 0, t: 5, w: 19, h: 3, bg: "#FFFFFF" }, { l: 26, t: 17, w: 6, h: 6, r: "50%", bg: "#FFFFFF" }] },
|
||||
br: { bg: "#009739", marks: [{ l: 8, t: 3, w: 24, h: 22, bg: "#FEDD00", clip: "polygon(50% 0, 100% 50%, 50% 100%, 0 50%)" }, { l: 15, t: 10, w: 9, h: 9, r: "50%", bg: "#012169" }] }
|
||||
};
|
||||
}
|
||||
|
||||
flag(code) {
|
||||
const h = React.createElement;
|
||||
const spec = this.flagSpecs()[code] || {};
|
||||
const style = { width: "40px", height: "28px", borderRadius: "5px", overflow: "hidden", display: "grid", border: "1px solid var(--border)", position: "relative", flex: "none" };
|
||||
let bands = [];
|
||||
if (spec.rows) {
|
||||
bands = spec.rows;
|
||||
style.gridTemplateRows = spec.sizes || "repeat(" + bands.length + ", 1fr)";
|
||||
} else if (spec.cols) {
|
||||
bands = spec.cols;
|
||||
style.gridAutoFlow = "column";
|
||||
style.gridTemplateColumns = spec.sizes || "repeat(" + bands.length + ", 1fr)";
|
||||
} else {
|
||||
style.background = spec.bg || "var(--surface-2)";
|
||||
}
|
||||
const kids = bands.map((c, i) => h("span", { key: "b" + i, style: { background: c, display: "block" } }));
|
||||
(spec.marks || []).forEach((m, i) => kids.push(h("span", {
|
||||
key: "m" + i,
|
||||
style: { position: "absolute", left: m.l + "px", top: m.t + "px", width: m.w + "px", height: m.h + "px", borderRadius: m.r || 0, background: m.bg, clipPath: m.clip || "none", display: "block" }
|
||||
})));
|
||||
return h("span", { style: style }, kids);
|
||||
}
|
||||
|
||||
qrImage(seed) {
|
||||
const n = 21;
|
||||
let h = 0;
|
||||
for (let i = 0; i < seed.length; i++) h = (h * 31 + seed.charCodeAt(i)) >>> 0;
|
||||
const cells = [];
|
||||
const finder = (r, c) => (r < 7 && c < 7) || (r < 7 && c >= n - 7) || (r >= n - 7 && c < 7);
|
||||
for (let r = 0; r < n; r++) {
|
||||
for (let c = 0; c < n; c++) {
|
||||
let on;
|
||||
if (finder(r, c)) {
|
||||
const rr = r < 7 ? r : n - 1 - r;
|
||||
const cc = c < 7 ? c : n - 1 - c;
|
||||
on = Math.max(Math.abs(rr - 3), Math.abs(cc - 3)) !== 2;
|
||||
} else {
|
||||
h = (h * 1103515245 + 12345) >>> 0;
|
||||
on = ((h >>> 16) & 1) === 1;
|
||||
}
|
||||
cells.push(React.createElement("span", { key: r + "-" + c, style: { background: on ? "var(--ink)" : "transparent", display: "block" } }));
|
||||
}
|
||||
}
|
||||
return React.createElement("div", {
|
||||
style: { display: "grid", gridTemplateColumns: "repeat(" + n + ", 1fr)", width: "196px", height: "196px", padding: "10px", borderRadius: "14px", background: "var(--bg)", border: "1px solid var(--border)" }
|
||||
}, cells);
|
||||
}
|
||||
|
||||
plural(n) {
|
||||
const m10 = n % 10, m100 = n % 100;
|
||||
if (m10 === 1 && m100 !== 11) return "сервер";
|
||||
if (m10 >= 2 && m10 <= 4 && (m100 < 10 || m100 >= 20)) return "сервера";
|
||||
return "серверов";
|
||||
}
|
||||
|
||||
renderVals() {
|
||||
const s = this.state;
|
||||
const sel = "var(--accent-soft)";
|
||||
const theme = s.theme || this.props.defaultTheme || "light";
|
||||
const keyData = [
|
||||
{ title: "Нидерланды · Амстердам", ip: "185.72.14.203", until: s.lang === "en" ? "14 October" : "14 октября", badge: "Активен", warn: false, left: "Продлится автоматически через 31 день", xray: "vless://8f2a…@nl1.veilix.net:443?type=xhttp#Veilix-NL" },
|
||||
{ title: "США · Нью-Йорк", ip: "23.105.61.44", until: s.lang === "en" ? "21 September" : "21 сентября", badge: "Истекает", warn: true, left: "Осталось 8 дней", xray: "vless://c41d…@us1.veilix.net:443?type=xhttp#Veilix-US" }
|
||||
];
|
||||
const cur = keyData[s.key];
|
||||
const cfgNames = [["Ноутбук", "подключено"], ["Телефон", "подключено"], ["Планшет", "подключено"], ["Рабочий ноутбук", "не занят"], ["Свободный слот", "не занят"]];
|
||||
const faqData = [
|
||||
["Чем это отличается от обычного VPN?", "У вас личный IP-адрес, который не делят с другими. Его не банят за чужую активность, и сервисы видят вас как обычного жителя выбранной страны."],
|
||||
["Что входит в цену?", "Индивидуальный IP в выбранной стране, выделенный RU-адрес, до 5 устройств, WireGuard и Amnezia WG, а также Xray с VLESS, Hysteria2 и XHTTP."],
|
||||
["Сколько устройств можно подключить?", "До пяти. На каждое — своя конфигурация WireGuard, IP-адрес при этом один и тот же."],
|
||||
["Как быстро выдаётся доступ?", "Если в локации есть свободные серверы — сразу после оплаты. Если нет, поднимем сервер под вас в течение 24 часов."],
|
||||
["Что если страна перестала подходить?", "Напишите в поддержку: перенесём ключ в другую локацию, доплата только за разницу в тарифе."],
|
||||
["Как платить и как отменить?", "Карта привязана к аккаунту, можно держать баланс и пополнять его картой, через СБП или криптой. Отмена — в разделе «Счёт и платежи», ключ доработает оплаченный месяц."]
|
||||
];
|
||||
|
||||
return {
|
||||
locCount: s.lang === "en" ? "30 countries" : "30 стран",
|
||||
locations: this.data().map((d, i) => ({
|
||||
country: d.country,
|
||||
flag: this.flag(d.code),
|
||||
sub: d.servers > 0 ? d.city + " · " + d.servers + " " + this.plural(d.servers) + " свободно" : d.city + " · установим за 24 часа",
|
||||
statusInk: d.servers > 0 ? "var(--accent)" : "var(--ink-3)",
|
||||
setup: d.setup,
|
||||
month: d.month,
|
||||
btn: d.servers > 0 ? "Купить" : "Заказать",
|
||||
btnBg: d.servers > 0 ? "var(--accent)" : "var(--surface-2)",
|
||||
btnInk: d.servers > 0 ? "var(--on-accent)" : "var(--ink-2)",
|
||||
buy: () => d.servers > 0 ? this.setState({ pay: d, paid: false }) : this.setState({ view: "lead" })
|
||||
})),
|
||||
faq: faqData.map((row, i) => ({
|
||||
q: row[0],
|
||||
a: row[1],
|
||||
off: s.faq !== i,
|
||||
rot: s.faq === i ? "rotate(-135deg) translateY(2px)" : "rotate(45deg) translateY(-2px)",
|
||||
toggle: () => this.setState(st => ({ faq: st.faq === i ? -1 : i }))
|
||||
})),
|
||||
keys: keyData.map((k, i) => ({
|
||||
title: k.title,
|
||||
ip: k.ip,
|
||||
untilLine: (s.lang === "en" ? "until " : "до ") + k.until,
|
||||
badge: k.badge,
|
||||
badgeBg: k.warn ? "color-mix(in oklab, var(--warn) 18%, transparent)" : sel,
|
||||
badgeInk: k.warn ? "var(--warn)" : "var(--accent)",
|
||||
open: () => this.setState({ view: "key", key: i })
|
||||
})),
|
||||
cur: {
|
||||
title: cur.title, ip: cur.ip, until: cur.until,
|
||||
left: cur.left, leftInk: cur.warn ? "var(--warn)" : "var(--ink-2)", xray: cur.xray
|
||||
},
|
||||
configs: cfgNames.map(c => ({
|
||||
name: c[0],
|
||||
state: c[1],
|
||||
showQr: () => this.setState({ qr: { title: "WireGuard · " + c[0], seed: cur.ip + c[0] } })
|
||||
})),
|
||||
goBuy: () => this.setState({ view: "buy" }),
|
||||
goFaq: () => this.setState({ view: "faq" }),
|
||||
goKeys: () => this.setState({ view: "keys" }),
|
||||
goMore: () => this.setState({ view: "more" }),
|
||||
goRef: () => this.setState({ view: "ref" }),
|
||||
goBilling: () => this.setState({ view: "billing" }),
|
||||
goLead: () => this.setState({ view: "lead" }),
|
||||
buyOff: s.view !== "buy",
|
||||
faqOff: s.view !== "faq",
|
||||
keysOff: s.view !== "keys",
|
||||
keyOff: s.view !== "key",
|
||||
moreOff: s.view !== "more",
|
||||
refOff: s.view !== "ref",
|
||||
billingOff: s.view !== "billing",
|
||||
leadOff: s.view !== "lead",
|
||||
tabBuyBg: s.view === "buy" ? sel : "transparent",
|
||||
tabFaqBg: s.view === "faq" ? sel : "transparent",
|
||||
tabKeysBg: (s.view === "keys" || s.view === "key") ? sel : "transparent",
|
||||
tabMoreBg: (s.view === "more" || s.view === "ref" || s.view === "billing" || s.view === "lead") ? sel : "transparent",
|
||||
tabBuyInk: s.view === "buy" ? "var(--accent)" : "var(--ink-2)",
|
||||
tabFaqInk: s.view === "faq" ? "var(--accent)" : "var(--ink-2)",
|
||||
tabKeysInk: (s.view === "keys" || s.view === "key") ? "var(--accent)" : "var(--ink-2)",
|
||||
tabMoreInk: (s.view === "more" || s.view === "ref" || s.view === "billing" || s.view === "lead") ? "var(--accent)" : "var(--ink-2)",
|
||||
openTopup: () => this.setState({ topup: true }),
|
||||
closeTopup: () => this.setState({ topup: false }),
|
||||
topupOff: !s.topup,
|
||||
sums: ["2 500 ₽", "5 000 ₽", "10 000 ₽"].map((label, i) => ({
|
||||
label: label,
|
||||
bg: s.sum === i ? sel : "transparent",
|
||||
border: s.sum === i ? "var(--accent)" : "var(--border)",
|
||||
pick: () => this.setState({ sum: i })
|
||||
})),
|
||||
methods: [
|
||||
{ label: "Карта ···· 4242", note: "мгновенно" },
|
||||
{ label: "СБП", note: "мгновенно" },
|
||||
{ label: "Криптовалюта", note: "USDT, BTC, ETH" }
|
||||
].map((m, i) => ({
|
||||
label: m.label,
|
||||
note: m.note,
|
||||
bg: s.method === i ? sel : "transparent",
|
||||
border: s.method === i ? "var(--accent)" : "var(--border)",
|
||||
pick: () => this.setState({ method: i })
|
||||
})),
|
||||
cryptoOff: s.method !== 2,
|
||||
sendTopup: () => this.setState({ topupSent: true }),
|
||||
topupButton: s.topupSent ? "Счёт выставлен" : "Пополнить",
|
||||
payments: [
|
||||
{ title: "Продление · Нидерланды", meta: "14 сентября · карта ···· 4242", amount: "−2 499 ₽", ink: "var(--ink)" },
|
||||
{ title: "Пополнение баланса", meta: "6 сентября · USDT (TRC-20)", amount: "+5 000 ₽", ink: "var(--accent)" },
|
||||
{ title: "Первый месяц · США", meta: "21 августа · карта ···· 4242", amount: "−4 900 ₽", ink: "var(--ink)" },
|
||||
{ title: "Продление · Нидерланды", meta: "14 августа · с баланса", amount: "−2 499 ₽", ink: "var(--ink)" },
|
||||
{ title: "Реферальный бонус", meta: "2 августа · месяц в подарок", amount: "+2 499 ₽", ink: "var(--accent)" }
|
||||
],
|
||||
sendLead: () => this.setState({ leadSent: true }),
|
||||
leadButton: s.leadSent ? "Заявка отправлена" : "Отправить заявку",
|
||||
refStats: s.lang === "en" ? "3 invited" : "3 друга",
|
||||
copyRef: () => this.setState({ refCopied: true }),
|
||||
refLabel: s.refCopied ? "Ссылка скопирована" : "Копировать ссылку",
|
||||
copyXray: () => this.setState({ copied: true }),
|
||||
copyLabel: s.copied ? "Скопировано" : "Копировать ключ",
|
||||
showXrayQr: () => this.setState({ qr: { title: "Xray · Happ", seed: cur.xray } }),
|
||||
payOff: !s.pay,
|
||||
payFlag: s.pay ? this.flag(s.pay.code) : null,
|
||||
payTitle: s.pay ? s.pay.country + " · " + s.pay.city : "",
|
||||
paySub: s.pay ? s.pay.servers + " " + this.plural(s.pay.servers) + " свободно · доступ сразу" : "",
|
||||
paySetup: s.pay ? s.pay.setup : "",
|
||||
payMonth: s.pay ? s.pay.month : "",
|
||||
payButton: s.paid ? "Оплата подтверждена" : (s.pay ? "Оплатить " + s.pay.setup : "Оплатить"),
|
||||
confirmPay: () => this.setState({ paid: true }),
|
||||
closePay: () => this.setState({ pay: null, paid: false }),
|
||||
stopPay: (e) => e.stopPropagation(),
|
||||
qrOff: !s.qr,
|
||||
qrTitle: s.qr ? s.qr.title : "",
|
||||
qrImage: s.qr ? this.qrImage(s.qr.seed) : null,
|
||||
closeQr: () => this.setState({ qr: null }),
|
||||
setRu: () => this.setLang("ru"),
|
||||
setEn: () => this.setLang("en"),
|
||||
ruBg: s.lang === "en" ? "transparent" : sel,
|
||||
enBg: s.lang === "en" ? sel : "transparent",
|
||||
ruInk: s.lang === "en" ? "var(--ink-3)" : "var(--ink)",
|
||||
enInk: s.lang === "en" ? "var(--ink)" : "var(--ink-3)",
|
||||
setLight: () => this.setTheme("light"),
|
||||
setDark: () => this.setTheme("dark"),
|
||||
lightBg: theme === "light" ? sel : "transparent",
|
||||
darkBg: theme === "dark" ? sel : "transparent"
|
||||
};
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,611 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<script src="./support.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<x-dc>
|
||||
<helmet>
|
||||
<link rel="icon" type="image/svg+xml" href="favicon.svg" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin="anonymous" />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Commissioner:wght@300;400;500;600&family=Unbounded:wght@500;600&display=swap" rel="stylesheet" />
|
||||
<script>(function(){if(window.__veilixI18nSrc)return;window.__veilixI18nSrc="./i18n.js?v="+Date.now().toString(36)+Math.random().toString(36).slice(2,8);var s=document.createElement("script");s.src=window.__veilixI18nSrc;document.head.appendChild(s);})();</script>
|
||||
<script src="https://unpkg.com/d3@7.9.0/dist/d3.min.js" integrity="sha384-CjloA8y00+1SDAUkjs099PVfnY2KmDC2BZnws9kh8D/lX1s46w6EPhpXdqMfjK6i" crossorigin="anonymous"></script>
|
||||
<script src="https://unpkg.com/topojson-client@3.1.0/dist/topojson-client.min.js" integrity="sha384-Ukv1p/xTma6P4/2bY5KzWBw+ydSpXmhCMtyciIQVDJ1RmOxtCYNMF1uXT9T63H67" crossorigin="anonymous"></script>
|
||||
<style>
|
||||
:root{
|
||||
--bg: oklch(99% 0.005 296);
|
||||
--surface: oklch(100% 0 0);
|
||||
--surface-2: oklch(97.5% 0.01 296);
|
||||
--ink: oklch(24% 0.04 296);
|
||||
--ink-2: oklch(45% 0.03 296);
|
||||
--ink-3: oklch(60% 0.025 296);
|
||||
--border: oklch(92% 0.014 296);
|
||||
--accent: oklch(45% 0.17 296);
|
||||
--accent-hover: oklch(38% 0.17 296);
|
||||
--accent-soft: oklch(95.5% 0.028 296);
|
||||
--on-accent: oklch(99% 0.01 296);
|
||||
--glow-1: oklch(86% 0.1 296);
|
||||
--glow-2: oklch(89% 0.09 266);
|
||||
--glow-3: oklch(91% 0.08 332);
|
||||
--veil: oklch(100% 0 0 / 0.55);
|
||||
--land: oklch(93% 0.015 296);
|
||||
--land-line: oklch(87% 0.02 296);
|
||||
--shadow: 0 1px 2px oklch(24% 0.04 296 / 0.05), 0 12px 32px -12px oklch(24% 0.04 296 / 0.14);
|
||||
}
|
||||
:root[data-veilix-theme="dark"]{
|
||||
--bg: oklch(17% 0.022 296);
|
||||
--surface: oklch(21% 0.026 296);
|
||||
--surface-2: oklch(24% 0.03 296);
|
||||
--ink: oklch(96% 0.01 296);
|
||||
--ink-2: oklch(79% 0.02 296);
|
||||
--ink-3: oklch(63% 0.025 296);
|
||||
--border: oklch(31% 0.032 296);
|
||||
--accent: oklch(74% 0.15 296);
|
||||
--accent-hover: oklch(80% 0.13 296);
|
||||
--accent-soft: oklch(27% 0.05 296);
|
||||
--on-accent: oklch(17% 0.03 296);
|
||||
--glow-1: oklch(45% 0.16 296);
|
||||
--glow-2: oklch(40% 0.14 266);
|
||||
--glow-3: oklch(42% 0.13 332);
|
||||
--veil: oklch(100% 0 0 / 0.07);
|
||||
--land: oklch(27% 0.03 296);
|
||||
--land-line: oklch(34% 0.035 296);
|
||||
--shadow: 0 1px 2px oklch(10% 0.02 296 / 0.4), 0 16px 40px -16px oklch(8% 0.02 296 / 0.7);
|
||||
}
|
||||
*{ box-sizing: border-box; }
|
||||
input, textarea, select{ font-size: 16px; }
|
||||
[hidden]{ display: none !important; }
|
||||
body{ margin:0; background: var(--bg); color: var(--ink); font-family:"Commissioner", system-ui, sans-serif; -webkit-font-smoothing: antialiased; }
|
||||
a{ color: var(--accent); text-decoration: none; }
|
||||
a:hover{ color: var(--accent-hover); }
|
||||
@keyframes veilPulse{
|
||||
0%{ transform: scale(1); opacity: 0.55; }
|
||||
70%{ transform: scale(2.6); opacity: 0; }
|
||||
100%{ transform: scale(2.6); opacity: 0; }
|
||||
}
|
||||
</style>
|
||||
</helmet>
|
||||
|
||||
<template id="__bundler_thumbnail" data-bg-color="#efeaf8">
|
||||
<svg viewBox="0 0 1200 800" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="480" y="290" width="150" height="150" rx="48" fill="#5b32c9" />
|
||||
<rect x="570" y="360" width="150" height="150" rx="48" fill="#ffffff" fill-opacity="0.62" stroke="#5b32c9" stroke-width="6" />
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<div style="background: var(--bg); color: var(--ink); font-family: 'Commissioner', system-ui, sans-serif; font-weight: 400; letter-spacing: -0.01em; min-height: 100vh;">
|
||||
|
||||
<header style="position: sticky; top: 0; z-index: 40; background: color-mix(in oklab, var(--bg) 82%, transparent); backdrop-filter: blur(20px) saturate(1.4); -webkit-backdrop-filter: blur(20px) saturate(1.4); border-bottom: 1px solid var(--border);">
|
||||
<div style="max-width: 1200px; margin: 0 auto; padding: 14px 24px; min-height: 66px; display: flex; flex-wrap: nowrap; align-items: center; gap: 14px 40px;">
|
||||
<a href="Main.dc.html" style="display: flex; align-items: center; gap: 10px; color: var(--ink); flex: none;">
|
||||
<span style="position: relative; width: 28px; height: 28px; display: block; flex: none;">
|
||||
<span style="position: absolute; left: 0; top: 3px; width: 21px; height: 21px; border-radius: 7px; background: linear-gradient(140deg, var(--accent), color-mix(in oklab, var(--accent) 55%, var(--glow-3))); display: block;"></span>
|
||||
<span style="position: absolute; right: 0; bottom: 3px; width: 21px; height: 21px; border-radius: 7px; background: var(--veil); border: 1px solid color-mix(in oklab, var(--accent) 35%, transparent); backdrop-filter: blur(2px); display: block;"></span>
|
||||
</span>
|
||||
<span style="font-family: 'Unbounded', 'Commissioner', sans-serif; font-weight: 600; font-size: 17px; letter-spacing: -0.02em; width: 88px; flex: none; white-space: nowrap;">Вейликс</span>
|
||||
</a>
|
||||
<nav hidden="{{ navOff }}" style="display: flex; flex-wrap: wrap; gap: 10px 28px; font-size: 15px; font-weight: 500; color: var(--ink-2);">
|
||||
<a href="Price.dc.html" style="color: var(--ink-2);" style-hover="color: var(--ink);">Цены</a>
|
||||
<a href="#top" style="color: var(--ink);">Локации</a>
|
||||
<a href="Support.dc.html" style="color: var(--ink-2);" style-hover="color: var(--ink);">Поддержка</a>
|
||||
</nav>
|
||||
<div style="margin-left: auto; display: flex; flex-wrap: wrap; align-items: center; gap: 8px;">
|
||||
<div hidden="{{ navOff }}" style="position: relative;">
|
||||
<button type="button" onClick="{{ toggleLangMenu }}" title="Язык" style="display: flex; align-items: center; gap: 7px; height: 38px; padding: 0 11px; border: 1px solid var(--border); border-radius: 10px; background: transparent; color: var(--ink-2); font-family: inherit; font-size: 14px; cursor: pointer;" style-hover="color: var(--ink); border-color: var(--ink-3);">
|
||||
<span style="position: relative; width: 16px; height: 16px; border-radius: 50%; border: 1.5px solid currentColor; display: block; flex: none;">
|
||||
<span style="position: absolute; left: -1.5px; top: 5.75px; width: 16px; height: 1.5px; background: currentColor; display: block;"></span>
|
||||
<span style="position: absolute; left: 3.25px; top: -1.5px; width: 8px; height: 16px; border-radius: 50%; border: 1.5px solid currentColor; display: block;"></span>
|
||||
</span>
|
||||
<span style="font-variant-numeric: tabular-nums;">{{ langLabel }}</span>
|
||||
</button>
|
||||
<div hidden="{{ langMenuOff }}" style="position: absolute; right: 0; top: 46px; width: 172px; padding: 6px; background: var(--surface); border: 1px solid var(--border); border-radius: 14px; box-shadow: var(--shadow); font-size: 14px;">
|
||||
<button type="button" onClick="{{ setRu }}" style="display: flex; align-items: center; justify-content: space-between; gap: 12px; width: 100%; padding: 9px 10px; border: 0; border-radius: 9px; background: {{ ruBg }}; color: var(--ink); font-family: inherit; font-size: 14px; text-align: left; cursor: pointer;">
|
||||
<span>Русский</span>
|
||||
<span style="color: var(--accent);">{{ ruCheck }}</span>
|
||||
</button>
|
||||
<button type="button" onClick="{{ setEn }}" style="display: flex; align-items: center; justify-content: space-between; gap: 12px; width: 100%; padding: 9px 10px; border: 0; border-radius: 9px; background: {{ enBg }}; color: var(--ink); font-family: inherit; font-size: 14px; text-align: left; cursor: pointer;">
|
||||
<span>English</span>
|
||||
<span style="color: var(--accent);">{{ enCheck }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<a href="App.dc.html" hidden="{{ navOff }}" style="display: flex; align-items: center; justify-content: center; height: 38px; width: 78px; flex: none; box-sizing: border-box; border-radius: 10px; color: var(--ink-2); font-size: 15px;" style-hover="color: var(--ink); background: var(--surface-2);">Войти</a>
|
||||
<a href="Order.dc.html" hidden="{{ navOff }}" style="display: flex; align-items: center; justify-content: center; height: 38px; width: 142px; flex: none; box-sizing: border-box; border-radius: 10px; background: var(--accent); color: var(--on-accent); font-size: 15px; font-weight: 500; box-shadow: 0 6px 18px -8px var(--accent);" style-hover="background: var(--accent-hover); color: var(--on-accent);">Получить VPN</a>
|
||||
<button type="button" hidden="{{ burgerOff }}" onClick="{{ toggleMenu }}" title="Меню" style="display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 5px; width: 42px; height: 38px; border: 1px solid var(--border); border-radius: 10px; background: transparent; color: var(--ink); cursor: pointer;" style-hover="border-color: var(--ink-3);">
|
||||
<span style="width: 18px; height: 2px; border-radius: 2px; background: currentColor; display: block;"></span>
|
||||
<span style="width: 18px; height: 2px; border-radius: 2px; background: currentColor; display: block;"></span>
|
||||
<span style="width: 18px; height: 2px; border-radius: 2px; background: currentColor; display: block;"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div hidden="{{ menuOff }}" style="position: absolute; left: 0; right: 0; top: 100%; z-index: 45; padding: 10px 20px 18px; background: var(--bg); border-bottom: 1px solid var(--border); box-shadow: var(--shadow); display: grid; gap: 2px;">
|
||||
<a href="Price.dc.html" onClick="{{ closeMenu }}" style="padding: 13px 6px; font-size: 17px; color: var(--ink);">Цены</a>
|
||||
<a href="#top" onClick="{{ closeMenu }}" style="padding: 13px 6px; font-size: 17px; color: var(--ink);">Локации</a>
|
||||
<a href="Support.dc.html" onClick="{{ closeMenu }}" style="padding: 13px 6px; font-size: 17px; color: var(--ink);">Поддержка</a>
|
||||
<div style="display: flex; align-items: center; justify-content: space-between; gap: 12px; padding-top: 6px; margin-top: 6px; border-top: 1px solid var(--border);">
|
||||
<a href="App.dc.html" onClick="{{ closeMenu }}" style="padding: 13px 6px; font-size: 17px; color: var(--ink);">Войти</a>
|
||||
<div style="display: flex; gap: 6px; flex: none;">
|
||||
<button type="button" onClick="{{ setRu }}" style="padding: 9px 13px; border: 1px solid var(--border); border-radius: 10px; background: {{ ruBg }}; color: var(--ink); font-family: inherit; font-size: 13px; cursor: pointer; white-space: nowrap;">Русский</button>
|
||||
<button type="button" onClick="{{ setEn }}" style="padding: 9px 13px; border: 1px solid var(--border); border-radius: 10px; background: {{ enBg }}; color: var(--ink); font-family: inherit; font-size: 13px; cursor: pointer; white-space: nowrap;">English</button>
|
||||
</div>
|
||||
</div>
|
||||
<a href="Order.dc.html" onClick="{{ closeMenu }}" style="display: flex; align-items: center; justify-content: center; height: 50px; margin-top: 12px; border-radius: 12px; background: var(--accent); color: var(--on-accent); font-size: 16px; font-weight: 500;">Получить VPN</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main id="top" style="max-width: 1200px; margin: 0 auto; padding: 0 clamp(14px, 4vw, 24px) 80px;">
|
||||
|
||||
<section style="position: relative; isolation: isolate; margin-top: clamp(14px, 3vw, 40px); border-radius: 28px; border: 1px solid var(--border); background: var(--surface); box-shadow: var(--shadow);">
|
||||
<div style="position: absolute; inset: 0; border-radius: 28px; overflow: hidden; pointer-events: none; -webkit-mask-image: -webkit-radial-gradient(white, black); mask-image: radial-gradient(white, black);">
|
||||
<div style="position: absolute; right: min(-90px, calc(-150px + 11vw)); top: min(-130px, calc(-190px + 11vw)); width: clamp(190px, 40vw, 440px); height: clamp(190px, 40vw, 440px); border-radius: 50%; background: radial-gradient(circle at 50% 50%, var(--glow-1) 0%, color-mix(in oklab, var(--glow-1) 35%, transparent) 55%, transparent 78%); filter: blur(80px); opacity: 0.45;"></div>
|
||||
<div style="position: absolute; left: min(-70px, calc(-130px + 11vw)); bottom: min(-150px, calc(-210px + 11vw)); width: clamp(160px, 34vw, 380px); height: clamp(160px, 34vw, 380px); border-radius: 50%; background: radial-gradient(circle at 50% 50%, var(--glow-3) 0%, color-mix(in oklab, var(--glow-3) 35%, transparent) 55%, transparent 78%); filter: blur(80px); opacity: 0.35;"></div>
|
||||
</div>
|
||||
<div style="position: relative; padding: clamp(22px, 5vw, 48px) clamp(14px, 3.5vw, 40px) clamp(20px, 4vw, 36px);">
|
||||
<h1 style="margin: 0 0 12px; font-size: clamp(34px, 4.4vw, 52px); font-weight: 600; letter-spacing: -0.035em;">Локации</h1>
|
||||
<p style="margin: 0 0 28px; max-width: 46em; font-size: 17px; line-height: 1.55; color: var(--ink-2); font-weight: 300;">30 стран на выбор и российский адрес в комплекте с любым тарифом.</p>
|
||||
<div id="veilix-map" style="width: 100%; min-height: 240px;"></div>
|
||||
<div style="margin-top: 10px; font-size: 13px; color: var(--ink-3);">Нажмите точку на карте — покажем страну и цену</div>
|
||||
<div hidden="{{ swipeOff }}" style="margin-top: 4px; font-size: 13px; color: var(--ink-3);">Карту можно прокрутить в сторону</div>
|
||||
<div style="display: flex; flex-wrap: wrap; gap: 10px 24px; margin-top: 20px; font-size: 14px; color: var(--ink-2);">
|
||||
<span style="display: inline-flex; align-items: center; gap: 8px;">
|
||||
<span style="width: 9px; height: 9px; border-radius: 50%; background: var(--accent); display: block;"></span>
|
||||
Серверы в наличии
|
||||
</span>
|
||||
<span style="display: inline-flex; align-items: center; gap: 8px;">
|
||||
<span style="width: 9px; height: 9px; border-radius: 50%; border: 1.5px solid var(--ink-3); display: block;"></span>
|
||||
Установим за 24 часа
|
||||
</span>
|
||||
<span style="display: inline-flex; align-items: center; gap: 8px;">
|
||||
<span style="width: 9px; height: 9px; border-radius: 2px; background: var(--ink-2); display: block;"></span>
|
||||
RU-адрес входит в каждый тариф
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section style="padding: 56px 0 0;">
|
||||
<h2 style="margin: 0 0 10px; font-size: clamp(26px, 2.8vw, 34px); font-weight: 600; letter-spacing: -0.03em;">Выберите страну</h2>
|
||||
<p style="margin: 0 0 24px; max-width: 52em; font-size: 15px; line-height: 1.55; color: var(--ink-2); font-weight: 300;">Базовая цена — 4 900 ₽ первый месяц и 2 499 ₽ далее. В редких локациях она выше и указана на карточке.</p>
|
||||
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 12px;">
|
||||
<sc-for list="{{ items }}" as="loc" hint-placeholder-count="12">
|
||||
<div role="button" tabindex="0" onClick="{{ loc.pick }}" style="display: grid; align-content: start; gap: 14px; padding: 22px; border-radius: 22px; border: 1px solid {{ loc.border }}; background: {{ loc.bg }}; color: var(--ink); cursor: pointer;" style-hover="border-color: var(--ink-3);">
|
||||
<span style="display: flex; align-items: center; justify-content: space-between; gap: 14px;">
|
||||
{{ loc.flag }}
|
||||
<span style="display: flex; align-items: center; gap: 12px; flex: none; min-height: 32px;">
|
||||
<span onClick="{{ loc.buy }}" hidden="{{ loc.buyOff }}" style="display: flex; align-items: center; justify-content: center; height: 32px; padding: 0 16px; border-radius: 10px; background: var(--accent); color: var(--on-accent); font-size: 14px; font-weight: 500; cursor: pointer;">Купить</span>
|
||||
<span hidden="{{ loc.dotOff }}" style="width: 11px; height: 11px; border-radius: 50%; background: {{ loc.dot }}; display: block; flex: none;"></span>
|
||||
</span>
|
||||
</span>
|
||||
<span style="display: grid; gap: 5px;">
|
||||
<span style="font-size: 19px; font-weight: 500; letter-spacing: -0.02em;">{{ loc.country }}</span>
|
||||
<span style="font-size: 14px; color: var(--ink-3);">{{ loc.city }}</span>
|
||||
</span>
|
||||
<span style="display: inline-flex; align-items: center; gap: 8px; font-size: 13px; color: {{ loc.statusInk }};">
|
||||
<span style="width: 6px; height: 6px; border-radius: 50%; background: {{ loc.statusDot }}; border: {{ loc.statusRing }}; display: block;"></span>
|
||||
{{ loc.status }}
|
||||
</span>
|
||||
<span style="display: grid; gap: 12px; padding-top: 14px; border-top: 1px solid var(--border);">
|
||||
<span style="display: flex; flex-wrap: wrap; align-items: baseline; gap: 6px; font-size: 13px; color: var(--ink-2); font-variant-numeric: tabular-nums;">
|
||||
<span style="font-size: 17px; font-weight: 500; color: var(--ink);">{{ loc.setup }}</span>
|
||||
первый месяц · далее
|
||||
<span style="font-weight: 500; color: var(--ink);">{{ loc.month }}</span>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</sc-for>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section style="padding: 56px 0 0;">
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 16px;">
|
||||
<div style="padding: clamp(20px, 5vw, 32px); border-radius: 22px; background: var(--surface); border: 1px solid var(--border); box-shadow: var(--shadow);">
|
||||
<h3 style="margin: 0 0 10px; font-size: 22px; font-weight: 600; letter-spacing: -0.025em;">Российский адрес в каждом тарифе</h3>
|
||||
<p style="margin: 0; font-size: 15px; line-height: 1.55; color: var(--ink-2); font-weight: 300;">Он работает в обе стороны: из России банки и госуслуги видят обычное местное подключение, из другой страны те же сервисы открываются так, будто вы дома.</p>
|
||||
</div>
|
||||
<div style="padding: clamp(20px, 5vw, 32px); border-radius: 22px; background: var(--surface); border: 1px solid var(--border); box-shadow: var(--shadow);">
|
||||
<h3 id="lead" style="margin: 0 0 10px; font-size: 22px; font-weight: 600; letter-spacing: -0.025em; scroll-margin-top: 90px;">Нужны индивидуальные условия</h3>
|
||||
<p style="margin: 0 0 18px; font-size: 15px; line-height: 1.55; color: var(--ink-2); font-weight: 300;">Нужной страны нет в списке, нужно много адресов или свой сценарий маршрутизации — напишите, скажем сроки и установим под вас.</p>
|
||||
<button type="button" onClick="{{ openLead }}" hidden="{{ leadOpen }}" style="display: inline-flex; align-items: center; height: 46px; padding: 0 20px; border-radius: 12px; border: 1px solid var(--accent); background: transparent; color: var(--accent); font-family: inherit; font-size: 15px; font-weight: 500; cursor: pointer;" style-hover="background: var(--accent-soft);">Оставить заявку</button>
|
||||
<div hidden="{{ leadClosed }}" style="display: grid; gap: 10px; max-width: 460px;">
|
||||
<input type="text" placeholder="Имя" style="height: 48px; padding: 0 14px; border-radius: 12px; border: 1px solid var(--border); background: var(--bg); color: var(--ink); font-family: inherit; font-size: 15px; width: 100%;" />
|
||||
<input type="email" placeholder="Email" style="height: 48px; padding: 0 14px; border-radius: 12px; border: 1px solid var(--border); background: var(--bg); color: var(--ink); font-family: inherit; font-size: 15px; width: 100%;" />
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 10px;">
|
||||
<input type="text" placeholder="Telegram" style="height: 48px; padding: 0 14px; border-radius: 12px; border: 1px solid var(--border); background: var(--bg); color: var(--ink); font-family: inherit; font-size: 15px; width: 100%;" />
|
||||
<input type="text" placeholder="WhatsApp" style="height: 48px; padding: 0 14px; border-radius: 12px; border: 1px solid var(--border); background: var(--bg); color: var(--ink); font-family: inherit; font-size: 15px; width: 100%;" />
|
||||
</div>
|
||||
<textarea rows="3" placeholder="Коротко о задаче" style="padding: 12px 14px; border-radius: 12px; border: 1px solid var(--border); background: var(--bg); color: var(--ink); font-family: inherit; font-size: 15px; resize: vertical; width: 100%;"></textarea>
|
||||
<button type="button" onClick="{{ sendLead }}" style="display: inline-flex; align-items: center; justify-content: center; height: 48px; padding: 0 22px; border-radius: 12px; border: 0; background: var(--accent); color: var(--on-accent); font-family: inherit; font-size: 15px; font-weight: 500; cursor: pointer;" style-hover="background: var(--accent-hover);">{{ leadButton }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer style="border-top: 1px solid var(--border); background: var(--surface-2);">
|
||||
<div style="max-width: 1200px; margin: 0 auto; padding: 48px clamp(16px, 4vw, 24px) 32px; display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 32px;">
|
||||
<div>
|
||||
<div style="display: flex; align-items: center; gap: 10px; margin-bottom: 14px;">
|
||||
<span style="position: relative; width: 24px; height: 24px; display: block; flex: none;">
|
||||
<span style="position: absolute; left: 0; top: 3px; width: 18px; height: 18px; border-radius: 6px; background: linear-gradient(140deg, var(--accent), color-mix(in oklab, var(--accent) 55%, var(--glow-3))); display: block;"></span>
|
||||
<span style="position: absolute; right: 0; bottom: 3px; width: 18px; height: 18px; border-radius: 6px; background: var(--veil); border: 1px solid color-mix(in oklab, var(--accent) 35%, transparent); display: block;"></span>
|
||||
</span>
|
||||
<span style="font-family: 'Unbounded', 'Commissioner', sans-serif; font-weight: 600; font-size: 15px; letter-spacing: -0.02em;">Вейликс</span>
|
||||
</div>
|
||||
<p style="margin: 0 0 20px; max-width: 26em; font-size: 14px; line-height: 1.55; color: var(--ink-3); font-weight: 300;">Персональные VPN-серверы с индивидуальным IP. Работаем с 2022 года.</p>
|
||||
<div style="display: flex; flex-wrap: wrap; gap: 6px;">
|
||||
<span style="display: flex; align-items: center; justify-content: center; width: 60px; height: 28px; border-radius: 8px; border: 1px solid var(--border); background: var(--surface); font-size: 11px; letter-spacing: 0.06em; color: var(--ink-3);">VISA</span>
|
||||
<span style="display: flex; align-items: center; justify-content: center; width: 60px; height: 28px; border-radius: 8px; border: 1px solid var(--border); background: var(--surface); font-size: 11px; letter-spacing: 0.06em; color: var(--ink-3);">MC</span>
|
||||
<span style="display: flex; align-items: center; justify-content: center; width: 60px; height: 28px; border-radius: 8px; border: 1px solid var(--border); background: var(--surface); font-size: 11px; letter-spacing: 0.06em; color: var(--ink-3);">МИР</span>
|
||||
<span style="display: flex; align-items: center; justify-content: center; width: 60px; height: 28px; border-radius: 8px; border: 1px solid var(--border); background: var(--surface); font-size: 11px; letter-spacing: 0.06em; color: var(--ink-3);">CRYPTO</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: grid; grid-template-columns: repeat(2, minmax(130px, 1fr)); gap: 24px 32px; align-content: start;">
|
||||
<div style="display: grid; grid-template-columns: minmax(0, 1fr); gap: 10px; align-content: start; font-size: 14px;">
|
||||
<div style="font-size: 13px; color: var(--ink-3); margin-bottom: 2px;">Сервис</div>
|
||||
<a href="Price.dc.html" style="color: var(--ink-2);" style-hover="color: var(--ink);">Цены</a>
|
||||
<a href="#top" style="color: var(--ink-2);" style-hover="color: var(--ink);">Локации</a>
|
||||
<a href="Support.dc.html" style="color: var(--ink-2);" style-hover="color: var(--ink);">Поддержка</a>
|
||||
</div>
|
||||
<div style="display: grid; grid-template-columns: minmax(0, 1fr); gap: 10px; align-content: start; font-size: 14px;">
|
||||
<div style="font-size: 13px; color: var(--ink-3); margin-bottom: 2px;">Документы</div>
|
||||
<a href="#top" style="color: var(--ink-2);" style-hover="color: var(--ink);">Условия использования</a>
|
||||
<a href="#top" style="color: var(--ink-2);" style-hover="color: var(--ink);">Политика конфиденциальности</a>
|
||||
<a href="#top" style="color: var(--ink-2);" style-hover="color: var(--ink);">Возврат средств</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="max-width: 1200px; margin: 0 auto; padding: 0 clamp(16px, 4vw, 24px) 40px; display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 14px;">
|
||||
<div style="font-size: 13px; color: var(--ink-3);">© 2026 Вейликс</div>
|
||||
<div style="display: flex; align-items: center; gap: 8px;">
|
||||
<div style="display: flex; padding: 2px; border-radius: 9px; border: 1px solid var(--border); background: var(--surface); flex: none;">
|
||||
<button type="button" onClick="{{ setLight }}" title="Светлая тема" style="display: flex; align-items: center; justify-content: center; width: 28px; height: 28px; border: 0; border-radius: 7px; background: {{ lightBg }}; color: {{ lightInk }}; cursor: pointer;"><span style="position: relative; width: 14px; height: 14px; display: block;"><span style="position: absolute; left: 3px; top: 3px; width: 8px; height: 8px; border-radius: 50%; background: currentColor; display: block;"></span><span style="position: absolute; left: 0; top: 0; width: 14px; height: 14px; border-radius: 50%; border: 1.5px solid currentColor; opacity: 0.45; display: block;"></span></span></button>
|
||||
<button type="button" onClick="{{ setDark }}" title="Тёмная тема" style="display: flex; align-items: center; justify-content: center; width: 28px; height: 28px; border: 0; border-radius: 7px; background: {{ darkBg }}; color: {{ darkInk }}; cursor: pointer;"><span style="width: 14px; height: 14px; border-radius: 50%; box-shadow: inset -5px -1.5px 0 0 currentColor; display: block;"></span></button>
|
||||
<button type="button" onClick="{{ setSystem }}" title="Как в системе" style="display: flex; align-items: center; justify-content: center; width: 28px; height: 28px; border: 0; border-radius: 7px; background: {{ systemBg }}; color: {{ systemInk }}; cursor: pointer;"><span style="width: 14px; height: 14px; border-radius: 50%; border: 1.5px solid currentColor; background: linear-gradient(90deg, currentColor 50%, transparent 50%); display: block;"></span></button>
|
||||
</div>
|
||||
<div style="display: flex; padding: 2px; border-radius: 9px; border: 1px solid var(--border); background: var(--surface); flex: none;">
|
||||
<button type="button" onClick="{{ setRu }}" style="padding: 0 10px; height: 28px; border: 0; border-radius: 7px; background: {{ ruBg }}; color: {{ ruInk }}; font-family: inherit; font-size: 12px; cursor: pointer;">RU</button>
|
||||
<button type="button" onClick="{{ setEn }}" style="padding: 0 10px; height: 28px; border: 0; border-radius: 7px; background: {{ enBg }}; color: {{ enInk }}; font-family: inherit; font-size: 12px; cursor: pointer;">EN</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</x-dc>
|
||||
<script type="text/x-dc" data-dc-script data-props="{"defaultTheme":{"editor":"enum","options":["light","dark","system"],"default":"light","tsType":"'light' | 'dark' | 'system'","section":"Тема"}}">
|
||||
class Component extends DCLogic {
|
||||
state = { theme: null, sysDark: false, lang: "ru", langMenu: false, narrow: false, menu: false, loc: 0, lead: false, leadSent: false };
|
||||
|
||||
data() {
|
||||
const base = { setup: "4 900 ₽", month: "2 499 ₽" };
|
||||
const rare = { setup: "6 900 ₽", month: "3 499 ₽" };
|
||||
const far = { setup: "9 900 ₽", month: "4 999 ₽" };
|
||||
const L = (country, city, code, lng, lat, ready, tier) => ({ country, city, code, lng, lat, ready, setup: tier.setup, month: tier.month, rare: tier !== base });
|
||||
return [
|
||||
L("Нидерланды", "Амстердам", "nl", 4.9, 52.37, true, base),
|
||||
L("Германия", "Франкфурт", "de", 8.68, 50.11, true, base),
|
||||
L("США", "Нью-Йорк", "us", -74.01, 40.71, true, base),
|
||||
L("Великобритания", "Лондон", "gb", -0.13, 51.51, true, base),
|
||||
L("Франция", "Париж", "fr", 2.35, 48.86, true, base),
|
||||
L("Финляндия", "Хельсинки", "fi", 24.94, 60.17, true, base),
|
||||
L("Швеция", "Стокгольм", "se", 18.07, 59.33, true, base),
|
||||
L("Норвегия", "Осло", "no", 10.75, 59.91, false, base),
|
||||
L("Дания", "Копенгаген", "dk", 12.57, 55.68, false, base),
|
||||
L("Швейцария", "Цюрих", "ch", 8.54, 47.37, true, rare),
|
||||
L("Австрия", "Вена", "at", 16.37, 48.21, true, base),
|
||||
L("Испания", "Мадрид", "es", -3.7, 40.42, true, base),
|
||||
L("Италия", "Милан", "it", 9.19, 45.46, true, base),
|
||||
L("Польша", "Варшава", "pl", 21.01, 52.23, true, base),
|
||||
L("Чехия", "Прага", "cz", 14.44, 50.08, true, base),
|
||||
L("Румыния", "Бухарест", "ro", 26.1, 44.43, false, base),
|
||||
L("Литва", "Вильнюс", "lt", 25.28, 54.69, true, base),
|
||||
L("Латвия", "Рига", "lv", 24.11, 56.95, true, base),
|
||||
L("Эстония", "Таллин", "ee", 24.75, 59.44, false, base),
|
||||
L("Турция", "Стамбул", "tr", 28.98, 41.01, true, base),
|
||||
L("Казахстан", "Алматы", "kz", 76.89, 43.24, false, base),
|
||||
L("Армения", "Ереван", "am", 44.51, 40.18, false, base),
|
||||
L("Грузия", "Тбилиси", "ge", 44.8, 41.72, false, base),
|
||||
L("ОАЭ", "Дубай", "ae", 55.27, 25.2, true, rare),
|
||||
L("Сингапур", "Сингапур", "sg", 103.82, 1.35, true, rare),
|
||||
L("Япония", "Токио", "jp", 139.69, 35.69, true, rare),
|
||||
L("Южная Корея", "Сеул", "kr", 126.98, 37.57, false, rare),
|
||||
L("Канада", "Торонто", "ca", -79.38, 43.65, true, base),
|
||||
L("Австралия", "Сидней", "au", 151.21, -33.87, false, far),
|
||||
L("Бразилия", "Сан-Паулу", "br", -46.63, -23.55, false, far)
|
||||
];
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const mq = window.matchMedia("(prefers-color-scheme: dark)");
|
||||
this.mq = mq;
|
||||
this.onSys = () => this.setState({ sysDark: mq.matches });
|
||||
mq.addEventListener("change", this.onSys);
|
||||
this.onResize = () => {
|
||||
const n = window.innerWidth < 900;
|
||||
if (n !== this.state.narrow) this.setState({ narrow: n, menu: false });
|
||||
};
|
||||
window.addEventListener("resize", this.onResize);
|
||||
this.onResize();
|
||||
let saved = null;
|
||||
try { saved = localStorage.getItem("veilix.theme"); } catch (e) {}
|
||||
let savedLang = null;
|
||||
try { savedLang = localStorage.getItem("veilix.lang"); } catch (e) {}
|
||||
this.setState({ sysDark: mq.matches, theme: saved || this.props.defaultTheme || "light", lang: savedLang || "ru" }, () => {
|
||||
this.apply();
|
||||
this.applyLang();
|
||||
});
|
||||
this.drawMap();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.mq) this.mq.removeEventListener("change", this.onSys);
|
||||
if (this.onResize) window.removeEventListener("resize", this.onResize);
|
||||
}
|
||||
|
||||
componentDidUpdate() { this.apply(); this.applyLang(); }
|
||||
|
||||
apply() {
|
||||
const theme = this.state.theme || this.props.defaultTheme || "light";
|
||||
const dark = theme === "dark" || (theme === "system" && this.state.sysDark);
|
||||
document.documentElement.setAttribute("data-veilix-theme", dark ? "dark" : "light");
|
||||
}
|
||||
|
||||
applyLang() {
|
||||
const lang = this.state.lang || "ru";
|
||||
if (window.VEILIX_APPLY_LANG) window.VEILIX_APPLY_LANG(document.body, lang);
|
||||
else setTimeout(() => this.applyLang(), 150);
|
||||
}
|
||||
|
||||
setTheme(t) {
|
||||
try { localStorage.setItem("veilix.theme", t); } catch (e) {}
|
||||
this.setState({ theme: t }, () => this.apply());
|
||||
}
|
||||
|
||||
setLang(l) {
|
||||
try { localStorage.setItem("veilix.lang", l); } catch (e) {}
|
||||
this.setState({ lang: l, langMenu: false }, () => this.applyLang());
|
||||
}
|
||||
|
||||
async drawMap() {
|
||||
const host = document.getElementById("veilix-map");
|
||||
if (!host) return;
|
||||
if (!window.d3 || !window.topojson) { setTimeout(() => this.drawMap(), 200); return; }
|
||||
const d3 = window.d3;
|
||||
let topo;
|
||||
try {
|
||||
topo = await d3.json("https://cdn.jsdelivr.net/npm/world-atlas@2.0.2/countries-110m.json");
|
||||
} catch (e) { return; }
|
||||
const land = window.topojson.feature(topo, topo.objects.countries);
|
||||
const W = 1200, H = 520;
|
||||
const projection = d3.geoNaturalEarth1().fitExtent([[10, 10], [W - 10, H - 10]], land);
|
||||
const path = d3.geoPath(projection);
|
||||
|
||||
host.innerHTML = "";
|
||||
const center = () => {
|
||||
const extra = host.scrollWidth - host.clientWidth;
|
||||
if (extra > 0) host.scrollLeft = extra / 2;
|
||||
};
|
||||
this.centerMap = center;
|
||||
if (!this.mapCentering) {
|
||||
this.mapCentering = true;
|
||||
window.addEventListener("resize", () => this.centerMap && this.centerMap());
|
||||
}
|
||||
host.style.overflowX = "auto";
|
||||
host.style.overflowY = "hidden";
|
||||
host.style.borderRadius = "18px";
|
||||
const svg = d3.select(host).append("svg")
|
||||
.attr("viewBox", "0 0 " + W + " " + H)
|
||||
.attr("width", "100%")
|
||||
.style("display", "block")
|
||||
.style("min-width", "820px")
|
||||
.style("overflow", "visible");
|
||||
|
||||
svg.append("g").selectAll("path").data(land.features).join("path")
|
||||
.attr("d", path)
|
||||
.attr("fill", "var(--land)")
|
||||
.attr("stroke", "var(--land-line)")
|
||||
.attr("stroke-width", 0.8);
|
||||
|
||||
const ru = projection([37.62, 55.75]);
|
||||
if (ru) {
|
||||
svg.append("rect")
|
||||
.attr("x", ru[0] - 5).attr("y", ru[1] - 5)
|
||||
.attr("width", 10).attr("height", 10).attr("rx", 2.5)
|
||||
.attr("fill", "var(--ink-2)");
|
||||
svg.append("text")
|
||||
.attr("x", ru[0] + 12).attr("y", ru[1] + 4)
|
||||
.attr("fill", "var(--ink-2)")
|
||||
.attr("font-size", 15)
|
||||
.attr("font-family", "Commissioner, system-ui, sans-serif")
|
||||
.text("RU");
|
||||
}
|
||||
|
||||
const FONT = "Commissioner, system-ui, sans-serif";
|
||||
const closePopup = () => { if (this.popupG) { this.popupG.remove(); this.popupG = null; } };
|
||||
this.closePopup = closePopup;
|
||||
svg.on("click", closePopup);
|
||||
|
||||
const popup = (d, p) => {
|
||||
closePopup();
|
||||
const pw = 268, ph = 138;
|
||||
const x = Math.max(8, Math.min(W - pw - 8, p[0] - pw / 2));
|
||||
let y = p[1] - ph - 16;
|
||||
if (y < 8) y = Math.min(H - ph - 8, p[1] + 18);
|
||||
const g = svg.append("g").attr("transform", "translate(" + x + "," + y + ")");
|
||||
this.popupG = g;
|
||||
g.on("click", (ev) => ev.stopPropagation());
|
||||
g.append("rect").attr("width", pw).attr("height", ph).attr("rx", 16)
|
||||
.attr("fill", "var(--surface)").attr("stroke", "var(--border)");
|
||||
g.append("text").attr("x", 20).attr("y", 33).attr("fill", "var(--ink)")
|
||||
.attr("font-size", 17).attr("font-weight", 500).attr("font-family", FONT).text(d.country);
|
||||
g.append("text").attr("x", 20).attr("y", 55).attr("fill", "var(--ink-3)")
|
||||
.attr("font-size", 13).attr("font-family", FONT).text(d.city);
|
||||
const price = g.append("text").attr("x", 20).attr("y", 83).attr("font-size", 13).attr("font-family", FONT);
|
||||
price.append("tspan").attr("fill", "var(--ink)").attr("font-weight", 500).text(d.setup);
|
||||
price.append("tspan").attr("dx", 6).attr("fill", "var(--ink-2)").text("первый месяц · далее");
|
||||
price.append("tspan").attr("dx", 6).attr("fill", "var(--ink)").attr("font-weight", 500).text(d.month);
|
||||
const link = g.append("a").attr("href", "Order.dc.html").style("cursor", "pointer");
|
||||
link.append("rect").attr("x", 20).attr("y", 97).attr("width", 108).attr("height", 30).attr("rx", 10).attr("fill", "var(--accent)");
|
||||
link.append("text").attr("x", 74).attr("y", 117).attr("text-anchor", "middle")
|
||||
.attr("fill", "var(--on-accent)").attr("font-size", 13).attr("font-weight", 500).attr("font-family", FONT).text("Купить");
|
||||
const close = g.append("g").style("cursor", "pointer").on("click", (ev) => { ev.stopPropagation(); closePopup(); });
|
||||
close.append("rect").attr("x", pw - 44).attr("y", 14).attr("width", 30).attr("height", 30).attr("rx", 9).attr("fill", "transparent");
|
||||
close.append("text").attr("x", pw - 29).attr("y", 35).attr("text-anchor", "middle")
|
||||
.attr("fill", "var(--ink-3)").attr("font-size", 18).attr("font-family", FONT).text("×");
|
||||
this.applyLang();
|
||||
};
|
||||
|
||||
this.data().forEach((d, i) => {
|
||||
const p = projection([d.lng, d.lat]);
|
||||
if (!p) return;
|
||||
const g = svg.append("g").attr("transform", "translate(" + p[0] + "," + p[1] + ")").style("cursor", "pointer");
|
||||
g.append("circle").attr("r", 12).attr("fill", "transparent");
|
||||
if (d.ready) {
|
||||
g.append("circle").attr("r", 6).attr("fill", "var(--accent)").attr("opacity", 0.32)
|
||||
.style("transform-box", "fill-box")
|
||||
.style("transform-origin", "center")
|
||||
.style("animation", "veilPulse 3.2s ease-out infinite");
|
||||
g.append("circle").attr("r", 5).attr("fill", "var(--accent)")
|
||||
.attr("stroke", "var(--surface)").attr("stroke-width", 1.4);
|
||||
} else {
|
||||
g.append("circle").attr("r", 5).attr("fill", "var(--bg)")
|
||||
.attr("stroke", "var(--ink-3)").attr("stroke-width", 2);
|
||||
}
|
||||
g.append("title").text(d.country + " · " + d.city);
|
||||
g.on("click", (ev) => { ev.stopPropagation(); this.setState({ loc: i }); popup(d, p); });
|
||||
});
|
||||
requestAnimationFrame(center);
|
||||
setTimeout(center, 120);
|
||||
}
|
||||
|
||||
flagSpecs() {
|
||||
return {
|
||||
nl: { rows: ["#AE1C28", "#FFFFFF", "#21468B"] },
|
||||
de: { rows: ["#000000", "#DD0000", "#FFCE00"] },
|
||||
us: { rows: ["#B22234", "#FFFFFF", "#B22234", "#FFFFFF", "#B22234", "#FFFFFF"], marks: [{ l: 0, t: 0, w: 22, h: 17, bg: "#3C3B6E" }] },
|
||||
gb: { bg: "#012169", marks: [{ l: 17, t: 0, w: 12, h: 32, bg: "#FFFFFF" }, { l: 0, t: 10, w: 46, h: 12, bg: "#FFFFFF" }, { l: 20, t: 0, w: 6, h: 32, bg: "#C8102E" }, { l: 0, t: 13, w: 46, h: 6, bg: "#C8102E" }] },
|
||||
fr: { cols: ["#002395", "#FFFFFF", "#ED2939"] },
|
||||
fi: { bg: "#FFFFFF", marks: [{ l: 13, t: 0, w: 8, h: 32, bg: "#003580" }, { l: 0, t: 12, w: 46, h: 8, bg: "#003580" }] },
|
||||
se: { bg: "#006AA7", marks: [{ l: 13, t: 0, w: 6, h: 32, bg: "#FECC00" }, { l: 0, t: 13, w: 46, h: 6, bg: "#FECC00" }] },
|
||||
no: { bg: "#BA0C2F", marks: [{ l: 12, t: 0, w: 10, h: 32, bg: "#FFFFFF" }, { l: 0, t: 11, w: 46, h: 10, bg: "#FFFFFF" }, { l: 15, t: 0, w: 4, h: 32, bg: "#00205B" }, { l: 0, t: 14, w: 46, h: 4, bg: "#00205B" }] },
|
||||
dk: { bg: "#C8102E", marks: [{ l: 13, t: 0, w: 6, h: 32, bg: "#FFFFFF" }, { l: 0, t: 13, w: 46, h: 6, bg: "#FFFFFF" }] },
|
||||
ch: { bg: "#D52B1E", marks: [{ l: 20, t: 8, w: 6, h: 16, bg: "#FFFFFF" }, { l: 15, t: 13, w: 16, h: 6, bg: "#FFFFFF" }] },
|
||||
at: { rows: ["#ED2939", "#FFFFFF", "#ED2939"] },
|
||||
es: { rows: ["#AA151B", "#F1BF00", "#AA151B"], sizes: "1fr 2fr 1fr" },
|
||||
it: { cols: ["#008C45", "#F4F5F0", "#CD212A"] },
|
||||
pl: { rows: ["#FFFFFF", "#DC143C"] },
|
||||
cz: { rows: ["#FFFFFF", "#D7141A"], marks: [{ l: 0, t: 0, w: 18, h: 32, bg: "#11457E", clip: "polygon(0 0, 100% 50%, 0 100%)" }] },
|
||||
ro: { cols: ["#002B7F", "#FCD116", "#CE1126"] },
|
||||
lt: { rows: ["#FDB913", "#006A44", "#C1272D"] },
|
||||
lv: { rows: ["#9E3039", "#FFFFFF", "#9E3039"], sizes: "2fr 1fr 2fr" },
|
||||
ee: { rows: ["#0072CE", "#000000", "#FFFFFF"] },
|
||||
tr: { bg: "#E30A17", marks: [{ l: 11, t: 8, w: 16, h: 16, r: "50%", bg: "#FFFFFF" }, { l: 16, t: 8, w: 16, h: 16, r: "50%", bg: "#E30A17" }] },
|
||||
kz: { bg: "#00AFCA", marks: [{ l: 16, t: 8, w: 15, h: 15, r: "50%", bg: "#FEC50C" }] },
|
||||
am: { rows: ["#D90012", "#0033A0", "#F2A800"] },
|
||||
ge: { bg: "#FFFFFF", marks: [{ l: 20, t: 0, w: 6, h: 32, bg: "#FF0000" }, { l: 0, t: 13, w: 46, h: 6, bg: "#FF0000" }] },
|
||||
ae: { rows: ["#00732F", "#FFFFFF", "#000000"], marks: [{ l: 0, t: 0, w: 11, h: 32, bg: "#FF0000" }] },
|
||||
sg: { rows: ["#EF3340", "#FFFFFF"], marks: [{ l: 5, t: 3, w: 11, h: 11, r: "50%", bg: "#FFFFFF" }, { l: 9, t: 3, w: 11, h: 11, r: "50%", bg: "#EF3340" }] },
|
||||
jp: { bg: "#FFFFFF", marks: [{ l: 16, t: 9, w: 14, h: 14, r: "50%", bg: "#BC002D" }] },
|
||||
kr: { bg: "#FFFFFF", marks: [{ l: 16, t: 9, w: 14, h: 14, r: "50%", bg: "#CD2E3A" }, { l: 16, t: 9, w: 14, h: 14, r: "50%", bg: "#0047A0", clip: "polygon(0 50%, 100% 50%, 100% 100%, 0 100%)" }] },
|
||||
ca: { cols: ["#D80621", "#FFFFFF", "#D80621"], sizes: "1fr 2fr 1fr", marks: [{ l: 20, t: 9, w: 7, h: 13, bg: "#D80621", clip: "polygon(50% 0, 100% 100%, 0 100%)" }] },
|
||||
au: { bg: "#00247D", marks: [{ l: 9, t: 0, w: 4, h: 16, bg: "#FFFFFF" }, { l: 0, t: 6, w: 22, h: 4, bg: "#FFFFFF" }, { l: 30, t: 19, w: 7, h: 7, r: "50%", bg: "#FFFFFF" }] },
|
||||
br: { bg: "#009739", marks: [{ l: 9, t: 4, w: 28, h: 24, bg: "#FEDD00", clip: "polygon(50% 0, 100% 50%, 50% 100%, 0 50%)" }, { l: 18, t: 11, w: 10, h: 10, r: "50%", bg: "#012169" }] }
|
||||
};
|
||||
}
|
||||
|
||||
flag(code) {
|
||||
const h = React.createElement;
|
||||
const spec = this.flagSpecs()[code] || {};
|
||||
const style = {
|
||||
width: "46px", height: "32px", borderRadius: "6px", overflow: "hidden",
|
||||
display: "grid", border: "1px solid var(--border)", position: "relative", flex: "none"
|
||||
};
|
||||
let bands = [];
|
||||
if (spec.rows) {
|
||||
bands = spec.rows;
|
||||
style.gridTemplateRows = spec.sizes || "repeat(" + bands.length + ", 1fr)";
|
||||
} else if (spec.cols) {
|
||||
bands = spec.cols;
|
||||
style.gridAutoFlow = "column";
|
||||
style.gridTemplateColumns = spec.sizes || "repeat(" + bands.length + ", 1fr)";
|
||||
} else {
|
||||
style.background = spec.bg || "var(--surface-2)";
|
||||
}
|
||||
const kids = bands.map((c, i) => h("span", { key: "b" + i, style: { background: c, display: "block" } }));
|
||||
(spec.marks || []).forEach((m, i) => kids.push(h("span", {
|
||||
key: "m" + i,
|
||||
style: {
|
||||
position: "absolute", left: m.l + "px", top: m.t + "px", width: m.w + "px", height: m.h + "px",
|
||||
borderRadius: m.r || 0, background: m.bg, clipPath: m.clip || "none", display: "block"
|
||||
}
|
||||
})));
|
||||
return h("span", { style: style }, kids);
|
||||
}
|
||||
|
||||
renderVals() {
|
||||
const s = this.state;
|
||||
const sel = "var(--accent-soft)";
|
||||
const theme = s.theme || this.props.defaultTheme || "light";
|
||||
const rows = this.data();
|
||||
const cur = rows[s.loc];
|
||||
return {
|
||||
items: rows.map((d, i) => ({
|
||||
country: d.country,
|
||||
city: d.city,
|
||||
flag: this.flag(d.code),
|
||||
status: d.ready ? "Серверы в наличии" : "Установим за 24 часа",
|
||||
statusInk: d.ready ? "var(--accent)" : "var(--ink-3)",
|
||||
statusDot: d.ready ? "var(--accent)" : "transparent",
|
||||
statusRing: d.ready ? "0" : "1.5px solid var(--ink-3)",
|
||||
bg: s.loc === i ? sel : "var(--surface)",
|
||||
border: s.loc === i ? "var(--accent)" : "var(--border)",
|
||||
dot: s.loc === i ? "var(--accent)" : "transparent",
|
||||
dotOff: s.loc === i,
|
||||
buy: (e) => { e.stopPropagation(); window.location.href = "Order.dc.html"; },
|
||||
setup: d.setup,
|
||||
month: d.month,
|
||||
buyOff: s.loc !== i,
|
||||
pick: () => this.setState({ loc: i })
|
||||
})),
|
||||
openLead: () => this.setState({ lead: true }),
|
||||
sendLead: () => this.setState({ leadSent: true }),
|
||||
leadOpen: this.state.lead,
|
||||
leadClosed: !this.state.lead,
|
||||
leadButton: this.state.leadSent ? "Заявка отправлена" : "Отправить",
|
||||
swipeOff: !s.narrow,
|
||||
navOff: s.narrow,
|
||||
burgerOff: !s.narrow,
|
||||
menuOff: !(s.narrow && s.menu),
|
||||
toggleMenu: () => this.setState(st => ({ menu: !st.menu, langMenu: false })),
|
||||
closeMenu: () => this.setState({ menu: false }),
|
||||
toggleLangMenu: () => this.setState(st => ({ langMenu: !st.langMenu })),
|
||||
langMenuOff: !s.langMenu,
|
||||
setRu: () => this.setLang("ru"),
|
||||
setEn: () => this.setLang("en"),
|
||||
langLabel: s.lang === "en" ? "EN" : "RU",
|
||||
ruBg: s.lang === "en" ? "transparent" : sel,
|
||||
enBg: s.lang === "en" ? sel : "transparent",
|
||||
ruInk: s.lang === "en" ? "var(--ink-3)" : "var(--ink)",
|
||||
enInk: s.lang === "en" ? "var(--ink)" : "var(--ink-3)",
|
||||
ruCheck: s.lang === "en" ? "" : "✓",
|
||||
enCheck: s.lang === "en" ? "✓" : "",
|
||||
setLight: () => this.setTheme("light"),
|
||||
setDark: () => this.setTheme("dark"),
|
||||
setSystem: () => this.setTheme("system"),
|
||||
lightBg: theme === "light" ? sel : "transparent",
|
||||
darkBg: theme === "dark" ? sel : "transparent",
|
||||
systemBg: theme === "system" ? sel : "transparent",
|
||||
lightInk: theme === "light" ? "var(--ink)" : "var(--ink-3)",
|
||||
darkInk: theme === "dark" ? "var(--ink)" : "var(--ink-3)",
|
||||
systemInk: theme === "system" ? "var(--ink)" : "var(--ink-3)"
|
||||
};
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,810 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<script src="./support.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<x-dc>
|
||||
<helmet>
|
||||
<link rel="icon" type="image/svg+xml" href="favicon.svg" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin="anonymous" />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Commissioner:wght@300;400;500;600&family=Unbounded:wght@500;600&display=swap" rel="stylesheet" />
|
||||
<script src="https://unpkg.com/d3@7.9.0/dist/d3.min.js" crossorigin="anonymous"></script>
|
||||
<script src="https://unpkg.com/topojson-client@3.1.0/dist/topojson-client.min.js" crossorigin="anonymous"></script>
|
||||
<script>(function(){if(window.__veilixI18nSrc)return;window.__veilixI18nSrc="./i18n.js?v="+Date.now().toString(36)+Math.random().toString(36).slice(2,8);var s=document.createElement("script");s.src=window.__veilixI18nSrc;document.head.appendChild(s);})();</script>
|
||||
<style>
|
||||
:root{
|
||||
--bg: oklch(99% 0.005 296);
|
||||
--surface: oklch(100% 0 0);
|
||||
--surface-2: oklch(97.5% 0.01 296);
|
||||
--ink: oklch(24% 0.04 296);
|
||||
--ink-2: oklch(45% 0.03 296);
|
||||
--ink-3: oklch(60% 0.025 296);
|
||||
--border: oklch(92% 0.014 296);
|
||||
--accent: oklch(45% 0.17 296);
|
||||
--accent-hover: oklch(38% 0.17 296);
|
||||
--accent-soft: oklch(95.5% 0.028 296);
|
||||
--on-accent: oklch(99% 0.01 296);
|
||||
--glow-1: oklch(86% 0.1 296);
|
||||
--glow-2: oklch(89% 0.09 266);
|
||||
--glow-3: oklch(91% 0.08 332);
|
||||
--veil: oklch(100% 0 0 / 0.55);
|
||||
--land: oklch(95% 0.012 296);
|
||||
--land-line: oklch(90% 0.018 296);
|
||||
--ocean: oklch(97.5% 0.007 296);
|
||||
--shadow: 0 1px 2px oklch(24% 0.04 296 / 0.05), 0 12px 32px -12px oklch(24% 0.04 296 / 0.14);
|
||||
}
|
||||
:root[data-veilix-theme="dark"]{
|
||||
--bg: oklch(17% 0.022 296);
|
||||
--surface: oklch(21% 0.026 296);
|
||||
--surface-2: oklch(24% 0.03 296);
|
||||
--ink: oklch(96% 0.01 296);
|
||||
--ink-2: oklch(79% 0.02 296);
|
||||
--ink-3: oklch(63% 0.025 296);
|
||||
--border: oklch(31% 0.032 296);
|
||||
--accent: oklch(74% 0.15 296);
|
||||
--accent-hover: oklch(80% 0.13 296);
|
||||
--accent-soft: oklch(27% 0.05 296);
|
||||
--on-accent: oklch(17% 0.03 296);
|
||||
--glow-1: oklch(45% 0.16 296);
|
||||
--glow-2: oklch(40% 0.14 266);
|
||||
--glow-3: oklch(42% 0.13 332);
|
||||
--veil: oklch(100% 0 0 / 0.07);
|
||||
--land: oklch(27% 0.028 296);
|
||||
--land-line: oklch(32% 0.032 296);
|
||||
--ocean: oklch(24% 0.025 296);
|
||||
--shadow: 0 1px 2px oklch(10% 0.02 296 / 0.4), 0 16px 40px -16px oklch(8% 0.02 296 / 0.7);
|
||||
}
|
||||
*{ box-sizing: border-box; }
|
||||
input, textarea, select{ font-size: 16px; }
|
||||
[hidden]{ display: none !important; }
|
||||
body{ margin:0; background: var(--bg); color: var(--ink); font-family:"Commissioner", system-ui, sans-serif; -webkit-font-smoothing: antialiased; }
|
||||
a{ color: var(--accent); text-decoration: none; }
|
||||
a:hover{ color: var(--accent-hover); }
|
||||
summary{ list-style: none; }
|
||||
@keyframes veilPulse{
|
||||
0%{ transform: scale(1); opacity: 0.32; }
|
||||
70%{ transform: scale(2.6); opacity: 0; }
|
||||
100%{ transform: scale(2.6); opacity: 0; }
|
||||
}
|
||||
@keyframes veilDrift{
|
||||
0%{ transform: translate3d(0,0,0) scale(1); }
|
||||
50%{ transform: translate3d(2%,-3%,0) scale(1.06); }
|
||||
100%{ transform: translate3d(0,0,0) scale(1); }
|
||||
}
|
||||
@keyframes veilDrift2{
|
||||
0%{ transform: translate3d(0,0,0) scale(1.04); }
|
||||
50%{ transform: translate3d(-3%,2%,0) scale(1); }
|
||||
100%{ transform: translate3d(0,0,0) scale(1.04); }
|
||||
}
|
||||
</style>
|
||||
</helmet>
|
||||
|
||||
<template id="__bundler_thumbnail" data-bg-color="#efeaf8">
|
||||
<svg viewBox="0 0 1200 800" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="480" y="290" width="150" height="150" rx="48" fill="#5b32c9" />
|
||||
<rect x="570" y="360" width="150" height="150" rx="48" fill="#ffffff" fill-opacity="0.62" stroke="#5b32c9" stroke-width="6" />
|
||||
</svg>
|
||||
</template>
|
||||
<div style="background: var(--bg); color: var(--ink); font-family: 'Commissioner', system-ui, sans-serif; font-weight: 400; letter-spacing: -0.01em; min-height: 100vh; transition: background 240ms ease, color 240ms ease;">
|
||||
|
||||
<header style="position: sticky; top: 0; z-index: 40; background: color-mix(in oklab, var(--bg) 82%, transparent); backdrop-filter: blur(20px) saturate(1.4); -webkit-backdrop-filter: blur(20px) saturate(1.4); border-bottom: 1px solid var(--border);">
|
||||
<div style="max-width: 1200px; margin: 0 auto; padding: 14px 24px; min-height: 66px; display: flex; flex-wrap: nowrap; align-items: center; gap: 14px 40px;">
|
||||
|
||||
<a href="#top" style="display: flex; align-items: center; gap: 10px; color: var(--ink); flex: none;">
|
||||
<span style="position: relative; width: 28px; height: 28px; display: block; flex: none;">
|
||||
<span hidden="{{ mark1Off }}" style="position: absolute; inset: 0;">
|
||||
<span style="position: absolute; left: 0; top: 3px; width: 21px; height: 21px; border-radius: 7px; background: linear-gradient(140deg, var(--accent), color-mix(in oklab, var(--accent) 55%, var(--glow-3))); display: block;"></span>
|
||||
<span style="position: absolute; right: 0; bottom: 3px; width: 21px; height: 21px; border-radius: 7px; background: var(--veil); border: 1px solid color-mix(in oklab, var(--accent) 35%, transparent); backdrop-filter: blur(2px); display: block;"></span>
|
||||
</span>
|
||||
<span hidden="{{ mark2Off }}" style="position: absolute; inset: 0; overflow: hidden;">
|
||||
<span style="position: absolute; left: 2px; top: 2px; width: 24px; height: 24px; border-radius: 50%; background: linear-gradient(150deg, var(--accent), color-mix(in oklab, var(--accent) 50%, var(--glow-2))); display: block;"></span>
|
||||
<span style="position: absolute; left: -6px; top: 13px; width: 40px; height: 18px; border-radius: 10px 10px 4px 4px; background: color-mix(in oklab, var(--bg) 72%, transparent); backdrop-filter: blur(3px); transform: rotate(-14deg); display: block;"></span>
|
||||
</span>
|
||||
<span hidden="{{ mark3Off }}" style="position: absolute; inset: 0;">
|
||||
<span style="position: absolute; left: 1px; top: 1px; width: 26px; height: 26px; border-radius: 50%; border: 2px solid var(--accent); border-bottom-color: transparent; border-right-color: transparent; transform: rotate(-30deg); display: block;"></span>
|
||||
<span style="position: absolute; left: 7px; top: 7px; width: 14px; height: 14px; border-radius: 50%; border: 2px solid color-mix(in oklab, var(--accent) 45%, transparent); border-bottom-color: transparent; border-right-color: transparent; transform: rotate(20deg); display: block;"></span>
|
||||
</span>
|
||||
<span hidden="{{ mark4Off }}" style="position: absolute; inset: 0;">
|
||||
<span style="position: absolute; left: 4px; top: 4px; width: 20px; height: 20px; border-radius: 6px; background: linear-gradient(135deg, var(--accent), color-mix(in oklab, var(--accent) 45%, var(--glow-1))); transform: rotate(45deg); display: block;"></span>
|
||||
<span style="position: absolute; left: 9px; top: 9px; width: 10px; height: 10px; border-radius: 3px; background: color-mix(in oklab, var(--bg) 85%, transparent); transform: rotate(45deg); display: block;"></span>
|
||||
</span>
|
||||
</span>
|
||||
<span style="font-family: 'Unbounded', 'Commissioner', sans-serif; font-weight: 600; font-size: 17px; letter-spacing: -0.02em; width: 88px; flex: none; white-space: nowrap;">Вейликс</span>
|
||||
</a>
|
||||
|
||||
<nav hidden="{{ navOff }}" style="display: flex; flex-wrap: wrap; gap: 10px 28px; font-size: 15px; font-weight: 500; color: var(--ink-2);">
|
||||
<a href="Price.dc.html" style="color: var(--ink-2);" style-hover="color: var(--ink);">Цены</a>
|
||||
<a href="Locations.dc.html" style="color: var(--ink-2);" style-hover="color: var(--ink);">Локации</a>
|
||||
<a href="Support.dc.html" style="color: var(--ink-2);" style-hover="color: var(--ink);">Поддержка</a>
|
||||
</nav>
|
||||
|
||||
<div style="margin-left: auto; display: flex; flex-wrap: wrap; align-items: center; gap: 8px;">
|
||||
<div hidden="{{ navOff }}" style="position: relative;">
|
||||
<button type="button" onClick="{{ toggleLangMenu }}" title="Язык" style="display: flex; align-items: center; gap: 7px; height: 38px; padding: 0 11px; border: 1px solid var(--border); border-radius: 10px; background: transparent; color: var(--ink-2); font-family: inherit; font-size: 14px; cursor: pointer;" style-hover="color: var(--ink); border-color: var(--ink-3);">
|
||||
<span style="position: relative; width: 16px; height: 16px; border-radius: 50%; border: 1.5px solid currentColor; display: block; flex: none;">
|
||||
<span style="position: absolute; left: -1.5px; top: 5.75px; width: 16px; height: 1.5px; background: currentColor; display: block;"></span>
|
||||
<span style="position: absolute; left: 3.25px; top: -1.5px; width: 8px; height: 16px; border-radius: 50%; border: 1.5px solid currentColor; display: block;"></span>
|
||||
</span>
|
||||
<span style="font-variant-numeric: tabular-nums;">{{ langLabel }}</span>
|
||||
</button>
|
||||
<div hidden="{{ langMenuOff }}" style="position: absolute; right: 0; top: 46px; width: 172px; padding: 6px; background: var(--surface); border: 1px solid var(--border); border-radius: 14px; box-shadow: var(--shadow); font-size: 14px;">
|
||||
<button type="button" onClick="{{ setRu }}" style="display: flex; align-items: center; justify-content: space-between; gap: 12px; width: 100%; padding: 9px 10px; border: 0; border-radius: 9px; background: {{ ruBg }}; color: var(--ink); font-family: inherit; font-size: 14px; text-align: left; cursor: pointer;">
|
||||
<span>Русский</span>
|
||||
<span style="color: var(--accent);">{{ ruCheck }}</span>
|
||||
</button>
|
||||
<button type="button" onClick="{{ setEn }}" style="display: flex; align-items: center; justify-content: space-between; gap: 12px; width: 100%; padding: 9px 10px; border: 0; border-radius: 9px; background: {{ enBg }}; color: var(--ink); font-family: inherit; font-size: 14px; text-align: left; cursor: pointer;">
|
||||
<span>English</span>
|
||||
<span style="color: var(--accent);">{{ enCheck }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<a href="App.dc.html" hidden="{{ navOff }}" style="display: flex; align-items: center; justify-content: center; height: 38px; width: 78px; flex: none; box-sizing: border-box; border-radius: 10px; color: var(--ink-2); font-size: 15px;" style-hover="color: var(--ink); background: var(--surface-2);">Войти</a>
|
||||
<a href="#prices" hidden="{{ navOff }}" style="display: flex; align-items: center; justify-content: center; height: 38px; width: 142px; flex: none; box-sizing: border-box; border-radius: 10px; background: var(--accent); color: var(--on-accent); font-size: 15px; font-weight: 500; box-shadow: 0 6px 18px -8px var(--accent);" style-hover="background: var(--accent-hover); color: var(--on-accent);">Получить VPN</a>
|
||||
<button type="button" hidden="{{ burgerOff }}" onClick="{{ toggleMenu }}" title="Меню" style="display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 5px; width: 42px; height: 38px; border: 1px solid var(--border); border-radius: 10px; background: transparent; color: var(--ink); cursor: pointer;" style-hover="border-color: var(--ink-3);">
|
||||
<span style="width: 18px; height: 2px; border-radius: 2px; background: currentColor; display: block;"></span>
|
||||
<span style="width: 18px; height: 2px; border-radius: 2px; background: currentColor; display: block;"></span>
|
||||
<span style="width: 18px; height: 2px; border-radius: 2px; background: currentColor; display: block;"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div hidden="{{ menuOff }}" style="position: absolute; left: 0; right: 0; top: 100%; z-index: 45; padding: 10px 20px 18px; background: var(--bg); border-bottom: 1px solid var(--border); box-shadow: var(--shadow); display: grid; gap: 2px;">
|
||||
<a href="Price.dc.html" onClick="{{ closeMenu }}" style="padding: 13px 6px; font-size: 17px; color: var(--ink);">Цены</a>
|
||||
<a href="Locations.dc.html" onClick="{{ closeMenu }}" style="padding: 13px 6px; font-size: 17px; color: var(--ink);">Локации</a>
|
||||
<a href="Support.dc.html" onClick="{{ closeMenu }}" style="padding: 13px 6px; font-size: 17px; color: var(--ink);">Поддержка</a>
|
||||
<div style="display: flex; align-items: center; justify-content: space-between; gap: 12px; padding-top: 6px; margin-top: 6px; border-top: 1px solid var(--border);">
|
||||
<a href="App.dc.html" onClick="{{ closeMenu }}" style="padding: 13px 6px; font-size: 17px; color: var(--ink);">Войти</a>
|
||||
<div style="display: flex; gap: 6px; flex: none;">
|
||||
<button type="button" onClick="{{ setRu }}" style="padding: 9px 13px; border: 1px solid var(--border); border-radius: 10px; background: {{ ruBg }}; color: var(--ink); font-family: inherit; font-size: 13px; cursor: pointer; white-space: nowrap;">Русский</button>
|
||||
<button type="button" onClick="{{ setEn }}" style="padding: 9px 13px; border: 1px solid var(--border); border-radius: 10px; background: {{ enBg }}; color: var(--ink); font-family: inherit; font-size: 13px; cursor: pointer; white-space: nowrap;">English</button>
|
||||
</div>
|
||||
</div>
|
||||
<a href="#prices" onClick="{{ closeMenu }}" style="display: flex; align-items: center; justify-content: center; height: 50px; margin-top: 12px; border-radius: 12px; background: var(--accent); color: var(--on-accent); font-size: 16px; font-weight: 500;">Получить VPN</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main id="top" style="max-width: 1200px; margin: 0 auto; padding: 0 24px;">
|
||||
|
||||
<section style="position: relative; min-height: 62vh; display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: clamp(28px, 5vw, 56px); align-items: center; padding: clamp(32px, 7vw, 72px) 0 clamp(28px, 7vw, 84px);">
|
||||
<div>
|
||||
<div style="display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin-bottom: 28px;">
|
||||
<div style="display: inline-flex; align-items: center; gap: 8px; padding: 6px 12px 6px 8px; border: 1px solid var(--border); border-radius: 999px; font-size: 13px; color: var(--ink-2);">
|
||||
<span style="width: 6px; height: 6px; border-radius: 50%; background: var(--accent); display: block;"></span>
|
||||
Персональный сервер, не общий
|
||||
</div>
|
||||
<div style="display: inline-flex; align-items: center; padding: 6px 12px; border-radius: 999px; background: var(--accent-soft); color: var(--accent); font-size: 13px; font-weight: 500;">Работаем с 2022 года</div>
|
||||
</div>
|
||||
<h1 style="margin: 0 0 22px; font-size: clamp(40px, 5.2vw, 62px); line-height: 1.04; font-weight: 600; letter-spacing: -0.035em; text-wrap: balance;">Ваш личный VPN с индивидуальным IP</h1>
|
||||
<p style="margin: 0 0 34px; max-width: 30em; font-size: 18px; line-height: 1.55; color: var(--ink-2); font-weight: 300; text-wrap: pretty;">Адрес принадлежит только вам: один и тот же IP на всех устройствах, в любой стране. Без соседей по серверу и их репутации.</p>
|
||||
<div style="display: flex; flex-wrap: wrap; align-items: center; gap: clamp(8px, 2vw, 12px);">
|
||||
<a href="#prices" style="display: flex; align-items: center; justify-content: center; height: 50px; padding: 0 clamp(14px, 4vw, 24px); border-radius: 12px; background: var(--accent); color: var(--on-accent); font-size: 16px; font-weight: 500; box-shadow: 0 10px 28px -12px var(--accent);" style-hover="background: var(--accent-hover); color: var(--on-accent);">Получить VPN</a>
|
||||
<a href="#locations" style="display: flex; align-items: center; justify-content: center; height: 50px; padding: 0 clamp(14px, 4vw, 22px); border-radius: 12px; border: 1px solid var(--border); color: var(--ink); font-size: 16px; white-space: nowrap;" style-hover="border-color: var(--ink-3); color: var(--ink);">Посмотреть локации</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="position: relative; aspect-ratio: 1 / 1; max-width: 480px; width: 100%; justify-self: {{ heroAlign }}; border-radius: 28px; overflow: hidden; isolation: isolate; -webkit-transform: translateZ(0); transform: translateZ(0); background: var(--surface-2); border: 1px solid var(--border);">
|
||||
<div style="position: absolute; left: -12%; top: -8%; width: 78%; height: 78%; border-radius: 50%; background: var(--glow-1); filter: blur(46px); opacity: 0.85; animation: veilDrift 16s ease-in-out infinite;"></div>
|
||||
<div style="position: absolute; right: -14%; top: 14%; width: 70%; height: 70%; border-radius: 50%; background: var(--glow-3); filter: blur(52px); opacity: 0.75; animation: veilDrift2 19s ease-in-out infinite;"></div>
|
||||
<div style="position: absolute; left: 16%; bottom: -16%; width: 82%; height: 68%; border-radius: 50%; background: var(--glow-2); filter: blur(54px); opacity: 0.8; animation: veilDrift 23s ease-in-out infinite;"></div>
|
||||
<div id="veilix-globe" style="position: absolute; left: 8%; top: 8%; width: 84%; height: 84%;"></div>
|
||||
<div style="position: absolute; left: 8%; top: 30%; right: 8%; height: 26%; border-radius: 18px; background: color-mix(in oklab, var(--veil) 72%, transparent); border: 1px solid color-mix(in oklab, var(--ink) 10%, transparent); backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px); transform: rotate(-8deg);"></div>
|
||||
<div style="position: absolute; left: 12%; top: 46%; right: 4%; height: 24%; border-radius: 18px; background: color-mix(in oklab, var(--veil) 72%, transparent); border: 1px solid color-mix(in oklab, var(--ink) 8%, transparent); backdrop-filter: blur(9px); -webkit-backdrop-filter: blur(9px); transform: rotate(6deg);"></div>
|
||||
<div style="position: absolute; left: 20px; right: 20px; bottom: 20px; display: grid; gap: 10px; padding: 14px 16px; border-radius: 16px; background: color-mix(in oklab, var(--surface) 84%, transparent); backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px); border: 1px solid var(--border);">
|
||||
<div style="display: flex; align-items: center; gap: 10px; font-size: 13px; color: var(--ink-2);">
|
||||
<span style="width: 7px; height: 7px; border-radius: 50%; background: var(--accent); display: block; flex: none;"></span>
|
||||
Ваш IP закреплён только за вами
|
||||
</div>
|
||||
<div style="display: flex; flex-wrap: wrap; gap: 6px 16px; padding-top: 10px; border-top: 1px solid var(--border); font-size: 13px; color: var(--ink-3); font-variant-numeric: tabular-nums;">
|
||||
<span>4 900 ₽ первый месяц, далее 2 499 ₽</span>
|
||||
<span>До 5 устройств</span>
|
||||
<span>RU-адрес в комплекте</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="advantages" style="padding: 16px 0 8px;">
|
||||
<div style="display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; gap: 24px; margin-bottom: 28px;">
|
||||
<h2 style="margin: 0; font-size: clamp(28px, 3vw, 38px); font-weight: 600; letter-spacing: -0.03em;">Что это даёт на практике</h2>
|
||||
</div>
|
||||
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 16px;">
|
||||
<div style="position: relative; isolation: isolate; padding: clamp(20px, 5vw, 32px); border-radius: 22px; background: var(--surface); border: 1px solid var(--border); box-shadow: var(--shadow);">
|
||||
<div style="position: absolute; inset: 0; border-radius: 22px; overflow: hidden; pointer-events: none; -webkit-mask-image: -webkit-radial-gradient(white, black); mask-image: radial-gradient(white, black);">
|
||||
<div style="position: absolute; right: -80px; top: -80px; width: 260px; height: 260px; border-radius: 50%; background: var(--glow-1); filter: blur(60px); opacity: 0.5;"></div>
|
||||
</div>
|
||||
<div style="position: relative;">
|
||||
<div style="display: flex; align-items: center; gap: 12px; margin-bottom: 18px;">
|
||||
<span style="position: relative; width: 34px; height: 34px; flex: none; display: block;">
|
||||
<span style="position: absolute; left: 11px; top: 3px; width: 12px; height: 12px; border-radius: 50%; background: var(--accent); display: block;"></span>
|
||||
<span style="position: absolute; left: 4px; top: 19px; width: 26px; height: 14px; border-radius: 13px 13px 4px 4px; background: var(--accent); opacity: 0.35; display: block;"></span>
|
||||
<span style="position: absolute; left: 23px; top: 18px; width: 11px; height: 11px; border-radius: 50%; border: 2px solid var(--accent); background: var(--surface); display: block;"></span>
|
||||
</span>
|
||||
<h3 style="margin: 0; font-size: 24px; font-weight: 600; letter-spacing: -0.025em;">Индивидуальный IP</h3>
|
||||
</div>
|
||||
<p style="margin: 0 0 26px; max-width: 34em; font-size: 16px; line-height: 1.55; color: var(--ink-2); font-weight: 300;">Адрес выделен под вас одного. Сервисы видят обычного человека из выбранной страны, а не сотню людей за одним адресом.</p>
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px 28px;">
|
||||
<div>
|
||||
<div style="font-size: 15px; font-weight: 500; margin-bottom: 6px;">Западные AI-сервисы</div>
|
||||
<div style="font-size: 14px; line-height: 1.5; color: var(--ink-2); font-weight: 300;">Риск блокировки ниже, чем у публичных VPN: их адреса уже давно в чёрных списках.</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size: 15px; font-weight: 500; margin-bottom: 6px;">Один адрес на всех устройствах</div>
|
||||
<div style="font-size: 14px; line-height: 1.5; color: var(--ink-2); font-weight: 300;">Ноутбук, телефон, планшет выглядят одинаково. Вы всегда в одной локации со своим IP.</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size: 15px; font-weight: 500; margin-bottom: 6px;">Меньше проверок и капч</div>
|
||||
<div style="font-size: 14px; line-height: 1.5; color: var(--ink-2); font-weight: 300;">На массовых адресах срабатывает антибот-защита. На личном сервисы работают стабильнее.</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size: 15px; font-weight: 500; margin-bottom: 6px;">Трафик под шифрованием</div>
|
||||
<div style="font-size: 14px; line-height: 1.5; color: var(--ink-2); font-weight: 300;">Дома, в отеле или в кафе соединение защищено в любой точке мира.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display: grid; gap: 16px; align-content: start;">
|
||||
<div style="padding: clamp(20px, 4.6vw, 26px); border-radius: 22px; background: var(--surface); border: 1px solid var(--border); box-shadow: var(--shadow);">
|
||||
<div style="display: flex; gap: 4px; margin-bottom: 16px;">
|
||||
<span style="width: 11px; height: 22px; border-radius: 3px; background: var(--accent); display: block;"></span>
|
||||
<span style="width: 11px; height: 22px; border-radius: 3px; background: color-mix(in oklab, var(--accent) 75%, transparent); display: block;"></span>
|
||||
<span style="width: 11px; height: 22px; border-radius: 3px; background: color-mix(in oklab, var(--accent) 55%, transparent); display: block;"></span>
|
||||
<span style="width: 11px; height: 22px; border-radius: 3px; background: color-mix(in oklab, var(--accent) 35%, transparent); display: block;"></span>
|
||||
<span style="width: 11px; height: 22px; border-radius: 3px; background: color-mix(in oklab, var(--accent) 18%, transparent); display: block;"></span>
|
||||
</div>
|
||||
<h3 style="margin: 0 0 8px; font-size: 19px; font-weight: 600; letter-spacing: -0.02em;">До 5 устройств в тарифе</h3>
|
||||
<p style="margin: 0; font-size: 14px; line-height: 1.5; color: var(--ink-2); font-weight: 300;">Нужно больше или другая страна — добавляете второй тариф со своим отдельным IP.</p>
|
||||
</div>
|
||||
|
||||
<div style="padding: clamp(20px, 4.6vw, 26px); border-radius: 22px; background: var(--surface); border: 1px solid var(--border); box-shadow: var(--shadow);">
|
||||
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 16px;">
|
||||
<span style="width: 22px; height: 22px; border-radius: 50%; background: var(--accent); display: block;"></span>
|
||||
<span style="width: 26px; height: 2px; background: color-mix(in oklab, var(--accent) 40%, transparent); display: block;"></span>
|
||||
<span style="width: 22px; height: 22px; border-radius: 50%; border: 2px solid color-mix(in oklab, var(--accent) 55%, transparent); display: block;"></span>
|
||||
</div>
|
||||
<h3 style="margin: 0 0 8px; font-size: 19px; font-weight: 600; letter-spacing: -0.02em;">Плюс выделенный RU-адрес</h3>
|
||||
<p style="margin: 0; font-size: 14px; line-height: 1.5; color: var(--ink-2); font-weight: 300;">Идёт к любому тарифу. Работает и как прямое соединение, и как мост в обе стороны.</p>
|
||||
</div>
|
||||
|
||||
<div style="padding: clamp(20px, 4.6vw, 26px); border-radius: 22px; background: var(--surface); border: 1px solid var(--border); box-shadow: var(--shadow);">
|
||||
<div style="display: flex; align-items: center; gap: 6px; margin-bottom: 16px;">
|
||||
<span style="width: 22px; height: 22px; border-radius: 7px; background: var(--accent); transform: rotate(45deg); display: block;"></span>
|
||||
<span style="width: 22px; height: 22px; border-radius: 7px; background: var(--veil); border: 1px solid color-mix(in oklab, var(--accent) 40%, transparent); transform: rotate(45deg); display: block;"></span>
|
||||
</div>
|
||||
<h3 style="margin: 0 0 8px; font-size: 19px; font-weight: 600; letter-spacing: -0.02em;">Два типа подключения</h3>
|
||||
<p style="margin: 0 0 12px; font-size: 14px; line-height: 1.5; color: var(--ink-2); font-weight: 300;">Если один способ перестал работать, второй уже настроен. Список дополняется и обновляется.</p>
|
||||
<div style="display: flex; flex-wrap: wrap; gap: 6px; font-size: 12px; color: var(--ink-3); font-variant-numeric: tabular-nums;">
|
||||
<span style="padding: 4px 9px; border-radius: 7px; background: var(--surface-2); border: 1px solid var(--border);">WireGuard / Amnezia WG</span>
|
||||
<span style="padding: 4px 9px; border-radius: 7px; background: var(--surface-2); border: 1px solid var(--border);">Xray: VLESS, Hysteria2, XHTTP</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 16px; padding: clamp(20px, 5vw, 32px); border-radius: 22px; background: var(--surface); border: 1px solid var(--border); box-shadow: var(--shadow);">
|
||||
<h3 style="margin: 0 0 6px; font-size: 24px; font-weight: 600; letter-spacing: -0.025em;">Трафик идёт коротким путём</h3>
|
||||
<p style="margin: 0 0 28px; max-width: 46em; font-size: 16px; line-height: 1.55; color: var(--ink-2); font-weight: 300;">Маршрут выбирается сам, по адресу сайта. Российские сервисы не ломаются, зарубежные открываются, переключать ничего не нужно.</p>
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 16px;">
|
||||
<div style="padding: 22px; border-radius: 16px; background: var(--surface-2); border: 1px solid var(--border);">
|
||||
<div style="font-size: 13px; color: var(--ink-3); margin-bottom: 16px;">Вы в России</div>
|
||||
<div style="display: grid; gap: 12px;">
|
||||
<div style="display: flex; flex-wrap: wrap; align-items: center; gap: 6px 12px; font-size: 14px;">
|
||||
<span style="flex: none; width: 72px; color: var(--ink-2); font-weight: 500;">RU-сайты</span>
|
||||
<span style="flex: 1; height: 1px; background: linear-gradient(90deg, var(--accent), color-mix(in oklab, var(--accent) 25%, transparent)); display: block;"></span>
|
||||
<span style="flex: none; color: var(--ink-2);">напрямую через RU-сервер</span>
|
||||
</div>
|
||||
<div style="display: flex; flex-wrap: wrap; align-items: center; gap: 6px 12px; font-size: 14px;">
|
||||
<span style="flex: none; width: 72px; color: var(--ink-2); font-weight: 500;">Остальной мир</span>
|
||||
<span style="flex: 1; height: 1px; background: repeating-linear-gradient(90deg, var(--accent) 0 6px, transparent 6px 12px); display: block;"></span>
|
||||
<span style="flex: none; color: var(--ink-2);">туннелем в вашу локацию</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="padding: 22px; border-radius: 16px; background: var(--surface-2); border: 1px solid var(--border);">
|
||||
<div style="font-size: 13px; color: var(--ink-3); margin-bottom: 16px;">Вы за границей</div>
|
||||
<div style="display: grid; gap: 12px;">
|
||||
<div style="display: flex; flex-wrap: wrap; align-items: center; gap: 6px 12px; font-size: 14px;">
|
||||
<span style="flex: none; width: 72px; color: var(--ink-2); font-weight: 500;">Мировые сайты</span>
|
||||
<span style="flex: 1; height: 1px; background: linear-gradient(90deg, var(--accent), color-mix(in oklab, var(--accent) 25%, transparent)); display: block;"></span>
|
||||
<span style="flex: none; color: var(--ink-2);">прямо из вашей локации</span>
|
||||
</div>
|
||||
<div style="display: flex; flex-wrap: wrap; align-items: center; gap: 6px 12px; font-size: 14px;">
|
||||
<span style="flex: none; width: 72px; color: var(--ink-2); font-weight: 500;">RU-сайты</span>
|
||||
<span style="flex: 1; height: 1px; background: repeating-linear-gradient(90deg, var(--accent) 0 6px, transparent 6px 12px); display: block;"></span>
|
||||
<span style="flex: none; color: var(--ink-2);">туннелем через RU-сервер</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-top: 16px; font-size: 13px; color: var(--ink-3);">Для тех, кто работает с Россией из другой страны.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="prices" style="padding: 72px 0 8px;">
|
||||
<div style="position: relative; overflow: hidden; isolation: isolate; -webkit-mask-image: -webkit-radial-gradient(white, black); mask-image: radial-gradient(white, black); -webkit-transform: translateZ(0); transform: translateZ(0); border-radius: 26px; border: 1px solid var(--border); background: var(--surface); box-shadow: var(--shadow);">
|
||||
<div style="position: absolute; left: min(-60px, calc(-110px + 8vw)); bottom: min(-120px, calc(-170px + 10vw)); width: clamp(150px, 34vw, 340px); height: clamp(150px, 34vw, 340px); border-radius: 50%; background: radial-gradient(circle at 60% 40%, var(--glow-1) 0%, color-mix(in oklab, var(--glow-1) 35%, transparent) 55%, transparent 78%); filter: blur(70px); opacity: 0.45;"></div>
|
||||
<div style="position: relative; display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: clamp(28px, 5vw, 40px); padding: clamp(20px, 5vw, 40px);">
|
||||
<div>
|
||||
<div style="display: flex; align-items: baseline; gap: 10px; white-space: nowrap;">
|
||||
<span style="font-size: clamp(34px, 8vw, 68px); font-weight: 600; letter-spacing: -0.04em; line-height: 1; font-variant-numeric: tabular-nums;">{{ curSetup }}</span>
|
||||
<span style="font-size: clamp(14px, 3.6vw, 16px); color: var(--ink-2);">первый месяц</span>
|
||||
</div>
|
||||
<div style="margin-top: 10px; font-size: clamp(15px, 3.8vw, 17px); color: var(--ink-2); white-space: nowrap; font-variant-numeric: tabular-nums;">далее <span>{{ curMonth }}</span> в месяц</div>
|
||||
<div style="margin-top: 24px; display: grid; gap: 10px; font-size: 14px; color: var(--ink-2); font-weight: 300;">
|
||||
<div style="display: flex; gap: 10px;"><span style="color: var(--accent);">—</span>Индивидуальный IP в выбранной стране</div>
|
||||
<div style="display: flex; gap: 10px;"><span style="color: var(--accent);">—</span>Выделенный RU-адрес в комплекте</div>
|
||||
<div style="display: flex; gap: 10px;"><span style="color: var(--accent);">—</span>До 5 устройств</div>
|
||||
<div style="display: flex; gap: 10px;"><span style="color: var(--accent);">—</span><span style="display: grid; gap: 5px;"><span>2 типа подключения:</span><span style="display: flex; gap: 8px; color: var(--ink-2);"><span style="flex: none;">—</span><span>WireGuard / Amnezia WG</span></span><span style="display: flex; gap: 8px; color: var(--ink-2);"><span style="flex: none;">—</span><span>Xray: VLESS, Hysteria2, XHTTP</span></span></span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="locations">
|
||||
<div style="display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; gap: 16px; margin-bottom: 14px;">
|
||||
<div style="font-size: 15px; font-weight: 500;">Выберите локацию</div>
|
||||
<div style="font-size: 13px; color: var(--ink-3);">{{ locHint }}</div>
|
||||
</div>
|
||||
<div style="display: grid; gap: 8px; max-height: 420px; overflow-y: auto; padding-right: 4px;">
|
||||
<sc-for list="{{ items }}" as="loc" hint-placeholder-count="8">
|
||||
<div role="button" tabindex="0" onClick="{{ loc.pick }}" style="display: flex; flex-wrap: wrap; align-items: center; gap: 10px 14px; padding: 12px 14px; border-radius: 14px; border: 1px solid {{ loc.border }}; background: {{ loc.bg }}; color: var(--ink); font-size: 15px; cursor: pointer;" style-hover="border-color: var(--ink-3);">
|
||||
{{ loc.flag }}
|
||||
<span style="flex: 1; min-width: 110px; display: grid; gap: 2px;">
|
||||
<span>{{ loc.country }}</span>
|
||||
<span style="font-size: 13px; color: var(--ink-3);">{{ loc.city }}</span>
|
||||
</span>
|
||||
<span style="display: grid; gap: 2px; text-align: right; font-variant-numeric: tabular-nums;">
|
||||
<span style="font-size: 15px; font-weight: 500;">{{ loc.setup }}</span>
|
||||
<span style="font-size: 13px; color: var(--ink-3);">далее <span>{{ loc.month }}</span></span>
|
||||
</span>
|
||||
</div>
|
||||
</sc-for>
|
||||
</div>
|
||||
<div style="display: flex; flex-wrap: wrap; align-items: center; gap: 12px; margin-top: 20px;">
|
||||
<a href="Order.dc.html" style="display: flex; align-items: center; height: 48px; padding: 0 24px; border-radius: 12px; background: var(--accent); color: var(--on-accent); font-size: 16px; font-weight: 500; box-shadow: 0 10px 28px -12px var(--accent);" style-hover="background: var(--accent-hover); color: var(--on-accent);">Купить</a>
|
||||
<button type="button" onClick="{{ openLead }}" hidden="{{ leadOpen }}" style="display: flex; align-items: center; height: 48px; padding: 0 20px; border-radius: 12px; border: 1px solid var(--border); background: transparent; color: var(--ink); font-family: inherit; font-size: 15px; cursor: pointer;" style-hover="border-color: var(--ink-3);">Нужны индивидуальные условия</button>
|
||||
</div>
|
||||
<div id="lead" hidden="{{ leadClosed }}" style="display: grid; gap: 10px; max-width: 460px; margin-top: 20px; scroll-margin-top: 90px;">
|
||||
<div style="font-size: 15px; font-weight: 500;">Нужны индивидуальные условия</div>
|
||||
<input type="text" placeholder="Имя" style="height: 48px; padding: 0 14px; border-radius: 12px; border: 1px solid var(--border); background: var(--bg); color: var(--ink); font-family: inherit; font-size: 15px; width: 100%;" />
|
||||
<input type="email" placeholder="Email" style="height: 48px; padding: 0 14px; border-radius: 12px; border: 1px solid var(--border); background: var(--bg); color: var(--ink); font-family: inherit; font-size: 15px; width: 100%;" />
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 10px;">
|
||||
<input type="text" placeholder="Telegram" style="height: 48px; padding: 0 14px; border-radius: 12px; border: 1px solid var(--border); background: var(--bg); color: var(--ink); font-family: inherit; font-size: 15px; width: 100%;" />
|
||||
<input type="text" placeholder="WhatsApp" style="height: 48px; padding: 0 14px; border-radius: 12px; border: 1px solid var(--border); background: var(--bg); color: var(--ink); font-family: inherit; font-size: 15px; width: 100%;" />
|
||||
</div>
|
||||
<textarea rows="3" placeholder="Коротко о задаче" style="padding: 12px 14px; border-radius: 12px; border: 1px solid var(--border); background: var(--bg); color: var(--ink); font-family: inherit; font-size: 15px; resize: vertical; width: 100%;"></textarea>
|
||||
<button type="button" onClick="{{ sendLead }}" style="display: inline-flex; align-items: center; justify-content: center; height: 48px; padding: 0 22px; border-radius: 12px; border: 0; background: var(--accent); color: var(--on-accent); font-family: inherit; font-size: 15px; font-weight: 500; cursor: pointer;" style-hover="background: var(--accent-hover);">{{ leadButton }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="faq" style="padding: 72px 0 32px;">
|
||||
<h2 style="margin: 0 0 28px; font-size: clamp(28px, 3vw, 38px); font-weight: 600; letter-spacing: -0.03em;">Вопросы</h2>
|
||||
<div style="display: grid; gap: 10px; max-width: 860px;">
|
||||
|
||||
<div style="border: 1px solid var(--border); border-radius: 16px; background: var(--surface); overflow: hidden;">
|
||||
<button type="button" onClick="{{ faq0 }}" style="display: flex; align-items: center; gap: 16px; width: 100%; padding: 20px 22px; background: transparent; border: 0; color: var(--ink); font-family: inherit; font-size: 17px; font-weight: 500; text-align: left; cursor: pointer; letter-spacing: -0.015em;">
|
||||
<span style="flex: 1;">Чем это отличается от обычного VPN за 300 рублей?</span>
|
||||
<span style="flex: none; width: 10px; height: 10px; border-right: 1.5px solid var(--ink-3); border-bottom: 1.5px solid var(--ink-3); transform: {{ faq0Rot }}; transition: transform 180ms ease; display: block;"></span>
|
||||
</button>
|
||||
<div hidden="{{ faq0Off }}" style="padding: 0 22px 22px; font-size: 15px; line-height: 1.6; color: var(--ink-2); font-weight: 300; max-width: 62ch;">В массовом VPN ваш адрес делят сотни людей. Если кто-то из них попал в чёрный список, ограничения касаются всех. У вас адрес один и только ваш, поэтому сервисы относятся к нему как к домашнему интернету.</div>
|
||||
</div>
|
||||
|
||||
<div style="border: 1px solid var(--border); border-radius: 16px; background: var(--surface); overflow: hidden;">
|
||||
<button type="button" onClick="{{ faq1 }}" style="display: flex; align-items: center; gap: 16px; width: 100%; padding: 20px 22px; background: transparent; border: 0; color: var(--ink); font-family: inherit; font-size: 17px; font-weight: 500; text-align: left; cursor: pointer; letter-spacing: -0.015em;">
|
||||
<span style="flex: 1;">Почему некоторые локации готовы только через 24 часа?</span>
|
||||
<span style="flex: none; width: 10px; height: 10px; border-right: 1.5px solid var(--ink-3); border-bottom: 1.5px solid var(--ink-3); transform: {{ faq1Rot }}; transition: transform 180ms ease; display: block;"></span>
|
||||
</button>
|
||||
<div hidden="{{ faq1Off }}" style="padding: 0 22px 22px; font-size: 15px; line-height: 1.6; color: var(--ink-2); font-weight: 300; max-width: 62ch;">Мы не держим пул общих адресов. Если в нужной стране свободных серверов нет, мы устанавливаем сервер лично под вас и выдаём доступ в течение суток. Оплата за первый месяц начинается с момента выдачи.</div>
|
||||
</div>
|
||||
|
||||
<div style="border: 1px solid var(--border); border-radius: 16px; background: var(--surface); overflow: hidden;">
|
||||
<button type="button" onClick="{{ faq2 }}" style="display: flex; align-items: center; gap: 16px; width: 100%; padding: 20px 22px; background: transparent; border: 0; color: var(--ink); font-family: inherit; font-size: 17px; font-weight: 500; text-align: left; cursor: pointer; letter-spacing: -0.015em;">
|
||||
<span style="flex: 1;">Нужно ли что-то настраивать самому?</span>
|
||||
<span style="flex: none; width: 10px; height: 10px; border-right: 1.5px solid var(--ink-3); border-bottom: 1.5px solid var(--ink-3); transform: {{ faq2Rot }}; transition: transform 180ms ease; display: block;"></span>
|
||||
</button>
|
||||
<div hidden="{{ faq2Off }}" style="padding: 0 22px 22px; font-size: 15px; line-height: 1.6; color: var(--ink-2); font-weight: 300; max-width: 62ch;">Нет. В личном кабинете вы скачиваете готовый файл или сканируете QR-код с телефона. Маршрутизация уже настроена: российские сайты идут напрямую, зарубежные — через ваш сервер.</div>
|
||||
</div>
|
||||
|
||||
<div style="border: 1px solid var(--border); border-radius: 16px; background: var(--surface); overflow: hidden;">
|
||||
<button type="button" onClick="{{ faq3 }}" style="display: flex; align-items: center; gap: 16px; width: 100%; padding: 20px 22px; background: transparent; border: 0; color: var(--ink); font-family: inherit; font-size: 17px; font-weight: 500; text-align: left; cursor: pointer; letter-spacing: -0.015em;">
|
||||
<span style="flex: 1;">Сколько устройств можно подключить?</span>
|
||||
<span style="flex: none; width: 10px; height: 10px; border-right: 1.5px solid var(--ink-3); border-bottom: 1.5px solid var(--ink-3); transform: {{ faq3Rot }}; transition: transform 180ms ease; display: block;"></span>
|
||||
</button>
|
||||
<div hidden="{{ faq3Off }}" style="padding: 0 22px 22px; font-size: 15px; line-height: 1.6; color: var(--ink-2); font-weight: 300; max-width: 62ch;">До пяти на один тариф, все с одним и тем же адресом. Нужно больше устройств или вторая страна — берёте дополнительный тариф с отдельным индивидуальным IP.</div>
|
||||
</div>
|
||||
|
||||
<div style="border: 1px solid var(--border); border-radius: 16px; background: var(--surface); overflow: hidden;">
|
||||
<button type="button" onClick="{{ faq4 }}" style="display: flex; align-items: center; gap: 16px; width: 100%; padding: 20px 22px; background: transparent; border: 0; color: var(--ink); font-family: inherit; font-size: 17px; font-weight: 500; text-align: left; cursor: pointer; letter-spacing: -0.015em;">
|
||||
<span style="flex: 1;">Зачем мне ещё и российский адрес?</span>
|
||||
<span style="flex: none; width: 10px; height: 10px; border-right: 1.5px solid var(--ink-3); border-bottom: 1.5px solid var(--ink-3); transform: {{ faq4Rot }}; transition: transform 180ms ease; display: block;"></span>
|
||||
</button>
|
||||
<div hidden="{{ faq4Off }}" style="padding: 0 22px 22px; font-size: 15px; line-height: 1.6; color: var(--ink-2); font-weight: 300; max-width: 62ch;">Он работает в обе стороны. Из России — чтобы банки, госуслуги и стриминг видели обычное местное подключение. Из другой страны — чтобы те же сервисы открывались так, будто вы дома.</div>
|
||||
</div>
|
||||
|
||||
<div style="border: 1px solid var(--border); border-radius: 16px; background: var(--surface); overflow: hidden;">
|
||||
<button type="button" onClick="{{ faq5 }}" style="display: flex; align-items: center; gap: 16px; width: 100%; padding: 20px 22px; background: transparent; border: 0; color: var(--ink); font-family: inherit; font-size: 17px; font-weight: 500; text-align: left; cursor: pointer; letter-spacing: -0.015em;">
|
||||
<span style="flex: 1;">Что будет, если способ подключения перестанет работать?</span>
|
||||
<span style="flex: none; width: 10px; height: 10px; border-right: 1.5px solid var(--ink-3); border-bottom: 1.5px solid var(--ink-3); transform: {{ faq5Rot }}; transition: transform 180ms ease; display: block;"></span>
|
||||
</button>
|
||||
<div hidden="{{ faq5Off }}" style="padding: 0 22px 22px; font-size: 15px; line-height: 1.6; color: var(--ink-2); font-weight: 300; max-width: 62ch;">На вашем сервере одновременно работают два типа подключения. Если один способ начал блокироваться, вы переключаетесь на второй в том же кабинете. Набор протоколов мы дополняем и обновляем.</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section hidden="{{ logoPickerOff }}" style="padding: 40px 0 24px; border-top: 1px dashed var(--border);">
|
||||
<div style="display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; gap: 20px; margin-bottom: 18px;">
|
||||
<h2 style="margin: 0; font-size: 20px; font-weight: 600; letter-spacing: -0.02em;">Варианты знака</h2>
|
||||
<span style="font-size: 13px; color: var(--ink-3);">Нажмите, чтобы примерить в шапке</span>
|
||||
</div>
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(165px, 1fr)); gap: 12px;">
|
||||
<button type="button" onClick="{{ pickMark1 }}" style="display: grid; gap: 14px; justify-items: start; padding: 22px; border-radius: 18px; border: 1px solid var(--border); background: {{ mark1Bg }}; color: var(--ink); font-family: inherit; text-align: left; cursor: pointer;" style-hover="border-color: var(--ink-3);">
|
||||
<span style="position: relative; width: 40px; height: 40px; display: block;">
|
||||
<span style="position: absolute; left: 0; top: 4px; width: 30px; height: 30px; border-radius: 10px; background: linear-gradient(140deg, var(--accent), color-mix(in oklab, var(--accent) 55%, var(--glow-3))); display: block;"></span>
|
||||
<span style="position: absolute; right: 0; bottom: 4px; width: 30px; height: 30px; border-radius: 10px; background: var(--veil); border: 1px solid color-mix(in oklab, var(--accent) 35%, transparent); backdrop-filter: blur(3px); display: block;"></span>
|
||||
</span>
|
||||
<span style="font-size: 14px; font-weight: 500;">Слои</span>
|
||||
<span style="font-size: 13px; color: var(--ink-3); line-height: 1.4;">Одно скрыто за другим</span>
|
||||
</button>
|
||||
<button type="button" onClick="{{ pickMark2 }}" style="display: grid; gap: 14px; justify-items: start; padding: 22px; border-radius: 18px; border: 1px solid var(--border); background: {{ mark2Bg }}; color: var(--ink); font-family: inherit; text-align: left; cursor: pointer;" style-hover="border-color: var(--ink-3);">
|
||||
<span style="position: relative; width: 40px; height: 40px; display: block; overflow: hidden;">
|
||||
<span style="position: absolute; left: 3px; top: 3px; width: 34px; height: 34px; border-radius: 50%; background: linear-gradient(150deg, var(--accent), color-mix(in oklab, var(--accent) 50%, var(--glow-2))); display: block;"></span>
|
||||
<span style="position: absolute; left: -8px; top: 19px; width: 56px; height: 26px; border-radius: 14px 14px 6px 6px; background: color-mix(in oklab, var(--bg) 72%, transparent); backdrop-filter: blur(4px); transform: rotate(-14deg); display: block;"></span>
|
||||
</span>
|
||||
<span style="font-size: 14px; font-weight: 500;">Вуаль</span>
|
||||
<span style="font-size: 13px; color: var(--ink-3); line-height: 1.4;">Полупрозрачная ткань поверх</span>
|
||||
</button>
|
||||
<button type="button" onClick="{{ pickMark3 }}" style="display: grid; gap: 14px; justify-items: start; padding: 22px; border-radius: 18px; border: 1px solid var(--border); background: {{ mark3Bg }}; color: var(--ink); font-family: inherit; text-align: left; cursor: pointer;" style-hover="border-color: var(--ink-3);">
|
||||
<span style="position: relative; width: 40px; height: 40px; display: block;">
|
||||
<span style="position: absolute; left: 1px; top: 1px; width: 38px; height: 38px; border-radius: 50%; border: 2.5px solid var(--accent); border-bottom-color: transparent; border-right-color: transparent; transform: rotate(-30deg); display: block;"></span>
|
||||
<span style="position: absolute; left: 10px; top: 10px; width: 20px; height: 20px; border-radius: 50%; border: 2.5px solid color-mix(in oklab, var(--accent) 45%, transparent); border-bottom-color: transparent; border-right-color: transparent; transform: rotate(20deg); display: block;"></span>
|
||||
</span>
|
||||
<span style="font-size: 14px; font-weight: 500;">Дуги</span>
|
||||
<span style="font-size: 13px; color: var(--ink-3); line-height: 1.4;">Складка, лёгкий жест</span>
|
||||
</button>
|
||||
<button type="button" onClick="{{ pickMark4 }}" style="display: grid; gap: 14px; justify-items: start; padding: 22px; border-radius: 18px; border: 1px solid var(--border); background: {{ mark4Bg }}; color: var(--ink); font-family: inherit; text-align: left; cursor: pointer;" style-hover="border-color: var(--ink-3);">
|
||||
<span style="position: relative; width: 40px; height: 40px; display: block;">
|
||||
<span style="position: absolute; left: 6px; top: 6px; width: 28px; height: 28px; border-radius: 8px; background: linear-gradient(135deg, var(--accent), color-mix(in oklab, var(--accent) 45%, var(--glow-1))); transform: rotate(45deg); display: block;"></span>
|
||||
<span style="position: absolute; left: 13px; top: 13px; width: 14px; height: 14px; border-radius: 4px; background: color-mix(in oklab, var(--bg) 85%, transparent); transform: rotate(45deg); display: block;"></span>
|
||||
</span>
|
||||
<span style="font-size: 14px; font-weight: 500;">Диафрагма</span>
|
||||
<span style="font-size: 13px; color: var(--ink-3); line-height: 1.4;">Проём, через который видно</span>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer style="margin-top: 40px; border-top: 1px solid var(--border); background: var(--surface-2);">
|
||||
<div style="max-width: 1200px; margin: 0 auto; padding: 48px clamp(16px, 4vw, 24px) 32px; display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 32px;">
|
||||
<div>
|
||||
<div style="display: flex; align-items: center; gap: 10px; margin-bottom: 14px;">
|
||||
<span style="position: relative; width: 24px; height: 24px; display: block; flex: none;">
|
||||
<span hidden="{{ mark1Off }}" style="position: absolute; inset: 0;">
|
||||
<span style="position: absolute; left: 0; top: 3px; width: 18px; height: 18px; border-radius: 6px; background: linear-gradient(140deg, var(--accent), color-mix(in oklab, var(--accent) 55%, var(--glow-3))); display: block;"></span>
|
||||
<span style="position: absolute; right: 0; bottom: 3px; width: 18px; height: 18px; border-radius: 6px; background: var(--veil); border: 1px solid color-mix(in oklab, var(--accent) 35%, transparent); display: block;"></span>
|
||||
</span>
|
||||
<span hidden="{{ mark2Off }}" style="position: absolute; inset: 0; overflow: hidden;">
|
||||
<span style="position: absolute; left: 2px; top: 2px; width: 20px; height: 20px; border-radius: 50%; background: linear-gradient(150deg, var(--accent), color-mix(in oklab, var(--accent) 50%, var(--glow-2))); display: block;"></span>
|
||||
<span style="position: absolute; left: -5px; top: 11px; width: 34px; height: 15px; border-radius: 9px 9px 4px 4px; background: color-mix(in oklab, var(--surface-2) 72%, transparent); backdrop-filter: blur(3px); transform: rotate(-14deg); display: block;"></span>
|
||||
</span>
|
||||
<span hidden="{{ mark3Off }}" style="position: absolute; inset: 0;">
|
||||
<span style="position: absolute; left: 1px; top: 1px; width: 22px; height: 22px; border-radius: 50%; border: 2px solid var(--accent); border-bottom-color: transparent; border-right-color: transparent; transform: rotate(-30deg); display: block;"></span>
|
||||
<span style="position: absolute; left: 6px; top: 6px; width: 12px; height: 12px; border-radius: 50%; border: 2px solid color-mix(in oklab, var(--accent) 45%, transparent); border-bottom-color: transparent; border-right-color: transparent; transform: rotate(20deg); display: block;"></span>
|
||||
</span>
|
||||
<span hidden="{{ mark4Off }}" style="position: absolute; inset: 0;">
|
||||
<span style="position: absolute; left: 3px; top: 3px; width: 18px; height: 18px; border-radius: 5px; background: linear-gradient(135deg, var(--accent), color-mix(in oklab, var(--accent) 45%, var(--glow-1))); transform: rotate(45deg); display: block;"></span>
|
||||
<span style="position: absolute; left: 8px; top: 8px; width: 8px; height: 8px; border-radius: 3px; background: color-mix(in oklab, var(--surface-2) 85%, transparent); transform: rotate(45deg); display: block;"></span>
|
||||
</span>
|
||||
</span>
|
||||
<span style="font-family: 'Unbounded', 'Commissioner', sans-serif; font-weight: 600; font-size: 15px; letter-spacing: -0.02em;">Вейликс</span>
|
||||
</div>
|
||||
<p style="margin: 0 0 20px; max-width: 26em; font-size: 14px; line-height: 1.55; color: var(--ink-3); font-weight: 300;">Персональные VPN-серверы с индивидуальным IP. Работаем с 2022 года.</p>
|
||||
<div style="display: flex; flex-wrap: wrap; gap: 6px;">
|
||||
<span style="display: flex; align-items: center; justify-content: center; width: 60px; height: 28px; border-radius: 8px; border: 1px solid var(--border); background: var(--surface); font-size: 11px; letter-spacing: 0.06em; color: var(--ink-3);">VISA</span>
|
||||
<span style="display: flex; align-items: center; justify-content: center; width: 60px; height: 28px; border-radius: 8px; border: 1px solid var(--border); background: var(--surface); font-size: 11px; letter-spacing: 0.06em; color: var(--ink-3);">MC</span>
|
||||
<span style="display: flex; align-items: center; justify-content: center; width: 60px; height: 28px; border-radius: 8px; border: 1px solid var(--border); background: var(--surface); font-size: 11px; letter-spacing: 0.06em; color: var(--ink-3);">МИР</span>
|
||||
<span style="display: flex; align-items: center; justify-content: center; width: 60px; height: 28px; border-radius: 8px; border: 1px solid var(--border); background: var(--surface); font-size: 11px; letter-spacing: 0.06em; color: var(--ink-3);">CRYPTO</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display: grid; grid-template-columns: repeat(2, minmax(130px, 1fr)); gap: 24px 32px; align-content: start;">
|
||||
<div style="display: grid; gap: 10px; align-content: start; font-size: 14px;">
|
||||
<div style="font-size: 13px; color: var(--ink-3); margin-bottom: 2px;">Сервис</div>
|
||||
<a href="Price.dc.html" style="color: var(--ink-2);" style-hover="color: var(--ink);">Цены</a>
|
||||
<a href="Locations.dc.html" style="color: var(--ink-2);" style-hover="color: var(--ink);">Локации</a>
|
||||
<a href="Support.dc.html" style="color: var(--ink-2);" style-hover="color: var(--ink);">Поддержка</a>
|
||||
</div>
|
||||
|
||||
<div style="display: grid; gap: 10px; align-content: start; font-size: 14px;">
|
||||
<div style="font-size: 13px; color: var(--ink-3); margin-bottom: 2px;">Документы</div>
|
||||
<a href="#top" style="color: var(--ink-2);" style-hover="color: var(--ink);">Условия использования</a>
|
||||
<a href="#top" style="color: var(--ink-2);" style-hover="color: var(--ink);">Политика конфиденциальности</a>
|
||||
<a href="#top" style="color: var(--ink-2);" style-hover="color: var(--ink);">Возврат средств</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div style="max-width: 1200px; margin: 0 auto; padding: 0 clamp(16px, 4vw, 24px) 40px; display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 14px;">
|
||||
<div style="font-size: 13px; color: var(--ink-3);">© 2026 Вейликс</div>
|
||||
<div style="display: flex; align-items: center; gap: 8px;">
|
||||
<div style="display: flex; padding: 2px; border-radius: 9px; border: 1px solid var(--border); background: var(--surface); flex: none;">
|
||||
<button type="button" onClick="{{ setLight }}" title="Светлая тема" style="display: flex; align-items: center; justify-content: center; width: 28px; height: 28px; border: 0; border-radius: 7px; background: {{ lightBg }}; color: {{ lightInk }}; cursor: pointer;"><span style="position: relative; width: 14px; height: 14px; display: block;"><span style="position: absolute; left: 3px; top: 3px; width: 8px; height: 8px; border-radius: 50%; background: currentColor; display: block;"></span><span style="position: absolute; left: 0; top: 0; width: 14px; height: 14px; border-radius: 50%; border: 1.5px solid currentColor; opacity: 0.45; display: block;"></span></span></button>
|
||||
<button type="button" onClick="{{ setDark }}" title="Тёмная тема" style="display: flex; align-items: center; justify-content: center; width: 28px; height: 28px; border: 0; border-radius: 7px; background: {{ darkBg }}; color: {{ darkInk }}; cursor: pointer;"><span style="width: 14px; height: 14px; border-radius: 50%; box-shadow: inset -5px -1.5px 0 0 currentColor; display: block;"></span></button>
|
||||
<button type="button" onClick="{{ setSystem }}" title="Как в системе" style="display: flex; align-items: center; justify-content: center; width: 28px; height: 28px; border: 0; border-radius: 7px; background: {{ systemBg }}; color: {{ systemInk }}; cursor: pointer;"><span style="width: 14px; height: 14px; border-radius: 50%; border: 1.5px solid currentColor; background: linear-gradient(90deg, currentColor 50%, transparent 50%); display: block;"></span></button>
|
||||
</div>
|
||||
<div style="display: flex; padding: 2px; border-radius: 9px; border: 1px solid var(--border); background: var(--surface); flex: none;">
|
||||
<button type="button" onClick="{{ setRu }}" style="padding: 0 10px; height: 28px; border: 0; border-radius: 7px; background: {{ ruBg }}; color: {{ ruInk }}; font-family: inherit; font-size: 12px; cursor: pointer;">RU</button>
|
||||
<button type="button" onClick="{{ setEn }}" style="padding: 0 10px; height: 28px; border: 0; border-radius: 7px; background: {{ enBg }}; color: {{ enInk }}; font-family: inherit; font-size: 12px; cursor: pointer;">EN</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</x-dc>
|
||||
<script type="text/x-dc" data-dc-script data-props="{"defaultTheme":{"editor":"enum","options":["light","dark","system"],"default":"light","tsType":"'light' | 'dark' | 'system'","section":"Тема"},"logoMark":{"editor":"enum","options":["layers","veil","arcs","aperture"],"default":"layers","tsType":"string","section":"Логотип"},"showLogoPicker":{"editor":"boolean","default":false,"tsType":"boolean","section":"Логотип"}}">
|
||||
class Component extends DCLogic {
|
||||
state = { theme: null, sysDark: false, loc: 0, faq: 0, mark: null, langMenu: false, lang: "ru", narrow: false, stacked: false, menu: false, lead: false, leadSent: false };
|
||||
|
||||
data() {
|
||||
const base = { setup: "4 900 ₽", month: "2 499 ₽" };
|
||||
const rare = { setup: "6 900 ₽", month: "3 499 ₽" };
|
||||
const far = { setup: "9 900 ₽", month: "4 999 ₽" };
|
||||
const L = (country, city, code, lng, lat, ready, tier) => ({ country, city, code, lng, lat, ready, setup: tier.setup, month: tier.month, rare: tier !== base });
|
||||
return [
|
||||
L("Нидерланды", "Амстердам", "nl", 4.9, 52.37, true, base),
|
||||
L("Германия", "Франкфурт", "de", 8.68, 50.11, true, base),
|
||||
L("США", "Нью-Йорк", "us", -74.01, 40.71, true, base),
|
||||
L("Великобритания", "Лондон", "gb", -0.13, 51.51, true, base),
|
||||
L("Франция", "Париж", "fr", 2.35, 48.86, true, base),
|
||||
L("Финляндия", "Хельсинки", "fi", 24.94, 60.17, true, base),
|
||||
L("Швеция", "Стокгольм", "se", 18.07, 59.33, true, base),
|
||||
L("Норвегия", "Осло", "no", 10.75, 59.91, false, base),
|
||||
L("Дания", "Копенгаген", "dk", 12.57, 55.68, false, base),
|
||||
L("Швейцария", "Цюрих", "ch", 8.54, 47.37, true, rare),
|
||||
L("Австрия", "Вена", "at", 16.37, 48.21, true, base),
|
||||
L("Испания", "Мадрид", "es", -3.7, 40.42, true, base),
|
||||
L("Италия", "Милан", "it", 9.19, 45.46, true, base),
|
||||
L("Польша", "Варшава", "pl", 21.01, 52.23, true, base),
|
||||
L("Чехия", "Прага", "cz", 14.44, 50.08, true, base),
|
||||
L("Румыния", "Бухарест", "ro", 26.1, 44.43, false, base),
|
||||
L("Литва", "Вильнюс", "lt", 25.28, 54.69, true, base),
|
||||
L("Латвия", "Рига", "lv", 24.11, 56.95, true, base),
|
||||
L("Эстония", "Таллин", "ee", 24.75, 59.44, false, base),
|
||||
L("Турция", "Стамбул", "tr", 28.98, 41.01, true, base),
|
||||
L("Казахстан", "Алматы", "kz", 76.89, 43.24, false, base),
|
||||
L("Армения", "Ереван", "am", 44.51, 40.18, false, base),
|
||||
L("Грузия", "Тбилиси", "ge", 44.8, 41.72, false, base),
|
||||
L("ОАЭ", "Дубай", "ae", 55.27, 25.2, true, rare),
|
||||
L("Сингапур", "Сингапур", "sg", 103.82, 1.35, true, rare),
|
||||
L("Япония", "Токио", "jp", 139.69, 35.69, true, rare),
|
||||
L("Южная Корея", "Сеул", "kr", 126.98, 37.57, false, rare),
|
||||
L("Канада", "Торонто", "ca", -79.38, 43.65, true, base),
|
||||
L("Австралия", "Сидней", "au", 151.21, -33.87, false, far),
|
||||
L("Бразилия", "Сан-Паулу", "br", -46.63, -23.55, false, far)
|
||||
];
|
||||
}
|
||||
|
||||
flagSpecs() {
|
||||
return {
|
||||
nl: { rows: ["#AE1C28", "#FFFFFF", "#21468B"] },
|
||||
de: { rows: ["#000000", "#DD0000", "#FFCE00"] },
|
||||
us: { rows: ["#B22234", "#FFFFFF", "#B22234", "#FFFFFF", "#B22234", "#FFFFFF"], marks: [{ l: 0, t: 0, w: 22, h: 17, bg: "#3C3B6E" }] },
|
||||
gb: { bg: "#012169", marks: [{ l: 17, t: 0, w: 12, h: 32, bg: "#FFFFFF" }, { l: 0, t: 10, w: 46, h: 12, bg: "#FFFFFF" }, { l: 20, t: 0, w: 6, h: 32, bg: "#C8102E" }, { l: 0, t: 13, w: 46, h: 6, bg: "#C8102E" }] },
|
||||
fr: { cols: ["#002395", "#FFFFFF", "#ED2939"] },
|
||||
fi: { bg: "#FFFFFF", marks: [{ l: 13, t: 0, w: 8, h: 32, bg: "#003580" }, { l: 0, t: 12, w: 46, h: 8, bg: "#003580" }] },
|
||||
se: { bg: "#006AA7", marks: [{ l: 13, t: 0, w: 6, h: 32, bg: "#FECC00" }, { l: 0, t: 13, w: 46, h: 6, bg: "#FECC00" }] },
|
||||
no: { bg: "#BA0C2F", marks: [{ l: 12, t: 0, w: 10, h: 32, bg: "#FFFFFF" }, { l: 0, t: 11, w: 46, h: 10, bg: "#FFFFFF" }, { l: 15, t: 0, w: 4, h: 32, bg: "#00205B" }, { l: 0, t: 14, w: 46, h: 4, bg: "#00205B" }] },
|
||||
dk: { bg: "#C8102E", marks: [{ l: 13, t: 0, w: 6, h: 32, bg: "#FFFFFF" }, { l: 0, t: 13, w: 46, h: 6, bg: "#FFFFFF" }] },
|
||||
ch: { bg: "#D52B1E", marks: [{ l: 20, t: 8, w: 6, h: 16, bg: "#FFFFFF" }, { l: 15, t: 13, w: 16, h: 6, bg: "#FFFFFF" }] },
|
||||
at: { rows: ["#ED2939", "#FFFFFF", "#ED2939"] },
|
||||
es: { rows: ["#AA151B", "#F1BF00", "#AA151B"], sizes: "1fr 2fr 1fr" },
|
||||
it: { cols: ["#008C45", "#F4F5F0", "#CD212A"] },
|
||||
pl: { rows: ["#FFFFFF", "#DC143C"] },
|
||||
cz: { rows: ["#FFFFFF", "#D7141A"], marks: [{ l: 0, t: 0, w: 18, h: 32, bg: "#11457E", clip: "polygon(0 0, 100% 50%, 0 100%)" }] },
|
||||
ro: { cols: ["#002B7F", "#FCD116", "#CE1126"] },
|
||||
lt: { rows: ["#FDB913", "#006A44", "#C1272D"] },
|
||||
lv: { rows: ["#9E3039", "#FFFFFF", "#9E3039"], sizes: "2fr 1fr 2fr" },
|
||||
ee: { rows: ["#0072CE", "#000000", "#FFFFFF"] },
|
||||
tr: { bg: "#E30A17", marks: [{ l: 11, t: 8, w: 16, h: 16, r: "50%", bg: "#FFFFFF" }, { l: 16, t: 8, w: 16, h: 16, r: "50%", bg: "#E30A17" }] },
|
||||
kz: { bg: "#00AFCA", marks: [{ l: 16, t: 8, w: 15, h: 15, r: "50%", bg: "#FEC50C" }] },
|
||||
am: { rows: ["#D90012", "#0033A0", "#F2A800"] },
|
||||
ge: { bg: "#FFFFFF", marks: [{ l: 20, t: 0, w: 6, h: 32, bg: "#FF0000" }, { l: 0, t: 13, w: 46, h: 6, bg: "#FF0000" }] },
|
||||
ae: { rows: ["#00732F", "#FFFFFF", "#000000"], marks: [{ l: 0, t: 0, w: 11, h: 32, bg: "#FF0000" }] },
|
||||
sg: { rows: ["#EF3340", "#FFFFFF"], marks: [{ l: 5, t: 3, w: 11, h: 11, r: "50%", bg: "#FFFFFF" }, { l: 9, t: 3, w: 11, h: 11, r: "50%", bg: "#EF3340" }] },
|
||||
jp: { bg: "#FFFFFF", marks: [{ l: 16, t: 9, w: 14, h: 14, r: "50%", bg: "#BC002D" }] },
|
||||
kr: { bg: "#FFFFFF", marks: [{ l: 16, t: 9, w: 14, h: 14, r: "50%", bg: "#CD2E3A" }, { l: 16, t: 9, w: 14, h: 14, r: "50%", bg: "#0047A0", clip: "polygon(0 50%, 100% 50%, 100% 100%, 0 100%)" }] },
|
||||
ca: { cols: ["#D80621", "#FFFFFF", "#D80621"], sizes: "1fr 2fr 1fr", marks: [{ l: 20, t: 9, w: 7, h: 13, bg: "#D80621", clip: "polygon(50% 0, 100% 100%, 0 100%)" }] },
|
||||
au: { bg: "#00247D", marks: [{ l: 9, t: 0, w: 4, h: 16, bg: "#FFFFFF" }, { l: 0, t: 6, w: 22, h: 4, bg: "#FFFFFF" }, { l: 30, t: 19, w: 7, h: 7, r: "50%", bg: "#FFFFFF" }] },
|
||||
br: { bg: "#009739", marks: [{ l: 9, t: 4, w: 28, h: 24, bg: "#FEDD00", clip: "polygon(50% 0, 100% 50%, 50% 100%, 0 50%)" }, { l: 18, t: 11, w: 10, h: 10, r: "50%", bg: "#012169" }] }
|
||||
};
|
||||
}
|
||||
|
||||
flag(code) {
|
||||
const h = React.createElement;
|
||||
const spec = this.flagSpecs()[code] || {};
|
||||
const style = {
|
||||
width: "46px", height: "32px", borderRadius: "6px", overflow: "hidden",
|
||||
display: "grid", border: "1px solid var(--border)", position: "relative", flex: "none"
|
||||
};
|
||||
let bands = [];
|
||||
if (spec.rows) {
|
||||
bands = spec.rows;
|
||||
style.gridTemplateRows = spec.sizes || "repeat(" + bands.length + ", 1fr)";
|
||||
} else if (spec.cols) {
|
||||
bands = spec.cols;
|
||||
style.gridAutoFlow = "column";
|
||||
style.gridTemplateColumns = spec.sizes || "repeat(" + bands.length + ", 1fr)";
|
||||
} else {
|
||||
style.background = spec.bg || "var(--surface-2)";
|
||||
}
|
||||
const kids = bands.map((c, i) => h("span", { key: "b" + i, style: { background: c, display: "block" } }));
|
||||
(spec.marks || []).forEach((m, i) => kids.push(h("span", {
|
||||
key: "m" + i,
|
||||
style: {
|
||||
position: "absolute", left: m.l + "px", top: m.t + "px", width: m.w + "px", height: m.h + "px",
|
||||
borderRadius: m.r || 0, background: m.bg, clipPath: m.clip || "none", display: "block"
|
||||
}
|
||||
})));
|
||||
return h("span", { style: style }, kids);
|
||||
}
|
||||
|
||||
async drawGlobe() {
|
||||
const host = document.getElementById("veilix-globe");
|
||||
if (!host) return;
|
||||
if (!window.d3 || !window.topojson) { setTimeout(() => this.drawGlobe(), 200); return; }
|
||||
const d3 = window.d3;
|
||||
let topo;
|
||||
try {
|
||||
topo = await d3.json("https://cdn.jsdelivr.net/npm/world-atlas@2.0.2/countries-110m.json");
|
||||
} catch (e) { return; }
|
||||
const land = window.topojson.feature(topo, topo.objects.countries);
|
||||
const S = 440, r = S / 2 - 4;
|
||||
const projection = d3.geoOrthographic().rotate([-20, -22]).scale(r).translate([S / 2, S / 2]);
|
||||
const path = d3.geoPath(projection);
|
||||
host.innerHTML = "";
|
||||
const svg = d3.select(host).append("svg")
|
||||
.attr("viewBox", "0 0 " + S + " " + S)
|
||||
.attr("width", "100%").attr("height", "100%")
|
||||
.style("display", "block").style("overflow", "visible");
|
||||
svg.append("circle").attr("cx", S / 2).attr("cy", S / 2).attr("r", r)
|
||||
.attr("fill", "var(--ocean)").attr("stroke", "var(--border)");
|
||||
const graticule = d3.geoGraticule10 ? d3.geoGraticule10() : null;
|
||||
if (graticule) {
|
||||
svg.append("path").datum(graticule).attr("d", path)
|
||||
.attr("fill", "none").attr("stroke", "var(--land-line)")
|
||||
.attr("stroke-width", 0.5).attr("opacity", 0.5);
|
||||
}
|
||||
svg.append("g").selectAll("path").data(land.features).join("path")
|
||||
.attr("d", path).attr("fill", "var(--land)")
|
||||
.attr("stroke", "var(--land-line)").attr("stroke-width", 0.6);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.drawGlobe();
|
||||
const mq = window.matchMedia("(prefers-color-scheme: dark)");
|
||||
this.mq = mq;
|
||||
this.onSys = () => this.setState({ sysDark: mq.matches });
|
||||
mq.addEventListener("change", this.onSys);
|
||||
this.onResize = () => {
|
||||
const n = window.innerWidth < 900;
|
||||
const st = window.innerWidth < 700;
|
||||
if (n !== this.state.narrow) this.setState({ narrow: n, menu: false });
|
||||
if (st !== this.state.stacked) this.setState({ stacked: st });
|
||||
};
|
||||
window.addEventListener("resize", this.onResize);
|
||||
this.onResize();
|
||||
let saved = null;
|
||||
try { saved = localStorage.getItem("veilix.theme"); } catch (e) {}
|
||||
let savedLang = null;
|
||||
try { savedLang = localStorage.getItem("veilix.lang"); } catch (e) {}
|
||||
this.setState({ sysDark: mq.matches, theme: saved || this.props.defaultTheme || "light", lang: savedLang || "ru" }, () => this.applyLang());
|
||||
}
|
||||
|
||||
applyLang() {
|
||||
const lang = this.state.lang || "ru";
|
||||
if (window.VEILIX_APPLY_LANG) window.VEILIX_APPLY_LANG(document.body, lang);
|
||||
else setTimeout(() => this.applyLang(), 150);
|
||||
}
|
||||
|
||||
setLang(l) {
|
||||
try { localStorage.setItem("veilix.lang", l); } catch (e) {}
|
||||
this.setState({ lang: l, langMenu: false }, () => this.applyLang());
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.mq) this.mq.removeEventListener("change", this.onSys);
|
||||
if (this.onResize) window.removeEventListener("resize", this.onResize);
|
||||
}
|
||||
|
||||
componentDidUpdate() { this.apply(); this.applyLang(); }
|
||||
|
||||
apply() {
|
||||
const theme = this.state.theme || this.props.defaultTheme || "light";
|
||||
const dark = theme === "dark" || (theme === "system" && this.state.sysDark);
|
||||
document.documentElement.setAttribute("data-veilix-theme", dark ? "dark" : "light");
|
||||
}
|
||||
|
||||
setTheme(t) {
|
||||
try { localStorage.setItem("veilix.theme", t); } catch (e) {}
|
||||
this.setState({ theme: t }, () => this.apply());
|
||||
}
|
||||
|
||||
renderVals() {
|
||||
const theme = this.state.theme || this.props.defaultTheme || "light";
|
||||
const mark = this.state.mark || this.props.logoMark || "layers";
|
||||
const marks = ["layers", "veil", "arcs", "aperture"];
|
||||
const sel = "var(--accent-soft)";
|
||||
const v = {
|
||||
locHint: this.data()[this.state.loc].ready ? "Свободные серверы есть сейчас" : "Сервер установим под вас за 24 часа",
|
||||
openLead: () => this.setState({ lead: true }),
|
||||
sendLead: () => this.setState({ leadSent: true }),
|
||||
leadOpen: this.state.lead,
|
||||
leadClosed: !this.state.lead,
|
||||
leadButton: this.state.leadSent ? "Заявка отправлена" : "Отправить",
|
||||
curSetup: this.data()[this.state.loc].setup,
|
||||
curMonth: this.data()[this.state.loc].month,
|
||||
items: this.data().map((d, n) => ({
|
||||
country: d.country,
|
||||
city: d.city,
|
||||
flag: this.flag(d.code),
|
||||
setup: d.setup,
|
||||
month: d.month,
|
||||
bg: this.state.loc === n ? sel : "transparent",
|
||||
border: this.state.loc === n ? "var(--accent)" : "var(--border)",
|
||||
pick: () => this.setState({ loc: n })
|
||||
})),
|
||||
logoPickerOff: this.props.showLogoPicker !== true,
|
||||
langMenuOff: !this.state.langMenu,
|
||||
heroAlign: this.state.stacked ? "center" : "end",
|
||||
navOff: this.state.narrow,
|
||||
burgerOff: !this.state.narrow,
|
||||
menuOff: !(this.state.narrow && this.state.menu),
|
||||
toggleMenu: () => this.setState(s => ({ menu: !s.menu, langMenu: false })),
|
||||
closeMenu: () => this.setState({ menu: false }),
|
||||
setRu: () => this.setLang("ru"),
|
||||
setEn: () => this.setLang("en"),
|
||||
langLabel: this.state.lang === "en" ? "EN" : "RU",
|
||||
ruBg: this.state.lang === "en" ? "transparent" : sel,
|
||||
enBg: this.state.lang === "en" ? sel : "transparent",
|
||||
ruInk: this.state.lang === "en" ? "var(--ink-3)" : "var(--ink)",
|
||||
enInk: this.state.lang === "en" ? "var(--ink)" : "var(--ink-3)",
|
||||
ruCheck: this.state.lang === "en" ? "" : "✓",
|
||||
enCheck: this.state.lang === "en" ? "✓" : "",
|
||||
toggleLangMenu: () => this.setState(s => ({ langMenu: !s.langMenu })),
|
||||
setLight: () => this.setTheme("light"),
|
||||
setDark: () => this.setTheme("dark"),
|
||||
setSystem: () => this.setTheme("system"),
|
||||
lightBg: theme === "light" ? sel : "transparent",
|
||||
darkBg: theme === "dark" ? sel : "transparent",
|
||||
systemBg: theme === "system" ? sel : "transparent",
|
||||
lightInk: theme === "light" ? "var(--ink)" : "var(--ink-3)",
|
||||
darkInk: theme === "dark" ? "var(--ink)" : "var(--ink-3)",
|
||||
systemInk: theme === "system" ? "var(--ink)" : "var(--ink-3)"
|
||||
};
|
||||
for (let i = 0; i < 5; i++) {
|
||||
v["loc" + i + "Bg"] = this.state.loc === i ? sel : "transparent";
|
||||
v["loc" + i + "Dot"] = this.state.loc === i ? "var(--accent)" : "var(--ink-3)";
|
||||
v["pickLoc" + i] = () => this.setState({ loc: i });
|
||||
}
|
||||
for (let i = 0; i < 6; i++) {
|
||||
v["faq" + i] = () => this.setState(s => ({ faq: s.faq === i ? -1 : i }));
|
||||
v["faq" + i + "Off"] = this.state.faq !== i;
|
||||
v["faq" + i + "Rot"] = this.state.faq === i ? "rotate(-135deg) translateY(2px)" : "rotate(45deg) translateY(-2px)";
|
||||
}
|
||||
marks.forEach((name, idx) => {
|
||||
const n = idx + 1;
|
||||
v["mark" + n + "Off"] = mark !== name;
|
||||
v["mark" + n + "Bg"] = mark === name ? sel : "transparent";
|
||||
v["pickMark" + n] = () => this.setState({ mark: name });
|
||||
});
|
||||
return v;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,984 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<script src="./support.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<x-dc>
|
||||
<helmet>
|
||||
<link rel="icon" type="image/svg+xml" href="favicon-mass.svg" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin="anonymous" />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Commissioner:wght@300;400;500;600&family=Unbounded:wght@500;600&display=swap" rel="stylesheet" />
|
||||
<script>(function(){if(window.__veilixI18nSrc)return;window.__veilixI18nSrc="./i18n.js?v="+Date.now().toString(36)+Math.random().toString(36).slice(2,8);var s=document.createElement("script");s.src=window.__veilixI18nSrc;document.head.appendChild(s);})();</script>
|
||||
<style>
|
||||
:root{
|
||||
--bg: oklch(99.6% 0.0015 215);
|
||||
--surface: oklch(100% 0 0);
|
||||
--surface-2: oklch(98.4% 0.003 215);
|
||||
--ink: oklch(24% 0.04 204);
|
||||
--ink-2: oklch(45% 0.03 204);
|
||||
--ink-3: oklch(60% 0.025 204);
|
||||
--border: oklch(92% 0.014 200);
|
||||
--accent: oklch(48% 0.105 178);
|
||||
--accent-hover: oklch(41% 0.1 178);
|
||||
--accent-soft: oklch(95.5% 0.032 180);
|
||||
--sel: oklch(96.5% 0.016 190);
|
||||
--on-accent: oklch(99% 0.01 180);
|
||||
--glow-1: oklch(86% 0.085 190);
|
||||
--glow-2: oklch(88% 0.075 205);
|
||||
--glow-3: oklch(89% 0.07 172);
|
||||
--veil: oklch(100% 0 0 / 0.55);
|
||||
--warn: oklch(62% 0.14 62);
|
||||
--shadow: 0 1px 2px oklch(24% 0.04 204 / 0.05), 0 12px 32px -12px oklch(24% 0.04 204 / 0.14);
|
||||
}
|
||||
:root[data-veilix-theme="dark"]{
|
||||
--bg: oklch(23% 0.026 210);
|
||||
--surface: oklch(26.5% 0.028 210);
|
||||
--surface-2: oklch(30% 0.03 210);
|
||||
--ink: oklch(96% 0.012 196);
|
||||
--ink-2: oklch(80% 0.02 196);
|
||||
--ink-3: oklch(65% 0.025 196);
|
||||
--border: oklch(36% 0.04 204);
|
||||
--accent: oklch(76% 0.105 190);
|
||||
--accent-hover: oklch(83% 0.09 190);
|
||||
--accent-soft: oklch(31% 0.05 190);
|
||||
--sel: oklch(30% 0.03 195);
|
||||
--on-accent: oklch(20% 0.04 204);
|
||||
--glow-1: oklch(52% 0.09 200);
|
||||
--glow-2: oklch(47% 0.085 215);
|
||||
--glow-3: oklch(50% 0.08 180);
|
||||
--veil: oklch(100% 0 0 / 0.07);
|
||||
--warn: oklch(78% 0.13 62);
|
||||
--shadow: 0 1px 2px oklch(12% 0.03 204 / 0.4), 0 16px 40px -16px oklch(9% 0.03 204 / 0.7);
|
||||
}
|
||||
*{ box-sizing: border-box; }
|
||||
input, textarea, select{ font-size: 16px; }
|
||||
[hidden]{ display: none !important; }
|
||||
body{ margin:0; background: var(--bg); color: var(--ink); font-family:"Commissioner", system-ui, sans-serif; -webkit-font-smoothing: antialiased; }
|
||||
a{ color: var(--accent); text-decoration: none; }
|
||||
a:hover{ color: var(--accent-hover); }
|
||||
input::placeholder{ color: var(--ink-3); }
|
||||
</style>
|
||||
</helmet>
|
||||
|
||||
<template id="__bundler_thumbnail" data-bg-color="#e3f4f1">
|
||||
<svg viewBox="0 0 1200 800" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="480" y="290" width="150" height="150" rx="48" fill="#00705e" />
|
||||
<rect x="570" y="360" width="150" height="150" rx="48" fill="#ffffff" fill-opacity="0.62" stroke="#00705e" stroke-width="6" />
|
||||
</svg>
|
||||
</template>
|
||||
<div style="background: var(--bg); color: var(--ink); font-family: 'Commissioner', system-ui, sans-serif; letter-spacing: -0.01em; min-height: 100vh;">
|
||||
|
||||
<div hidden="{{ appOff }}" style="display: grid; grid-template-rows: 1fr auto; min-height: 100vh;">
|
||||
<div style="display: flex; align-items: center; justify-content: center; padding: 24px;">
|
||||
<div style="width: 100%; max-width: 420px; padding: 36px; border-radius: 24px; background: var(--surface); border: 1px solid var(--border); box-shadow: var(--shadow);">
|
||||
<div style="display: flex; align-items: center; gap: 10px; margin-bottom: 26px;">
|
||||
<span style="position: relative; width: 26px; height: 26px; display: block; flex: none;">
|
||||
<span style="position: absolute; left: 0; top: 3px; width: 19px; height: 19px; border-radius: 6px; background: linear-gradient(140deg, var(--accent), color-mix(in oklab, var(--accent) 55%, var(--glow-3))); display: block;"></span>
|
||||
<span style="position: absolute; right: 0; bottom: 3px; width: 19px; height: 19px; border-radius: 6px; background: var(--veil); border: 1px solid color-mix(in oklab, var(--accent) 35%, transparent); display: block;"></span>
|
||||
</span>
|
||||
<span style="font-family: 'Unbounded', 'Commissioner', sans-serif; font-weight: 600; font-size: 16px; letter-spacing: -0.02em;">ВейликсВПН</span>
|
||||
</div>
|
||||
<h1 style="margin: 0 0 10px; font-size: 26px; font-weight: 600; letter-spacing: -0.03em;">Вход в кабинет</h1>
|
||||
<p style="margin: 0 0 22px; font-size: 15px; line-height: 1.55; color: var(--ink-2); font-weight: 300;">Введите почту, пришлём одноразовый код. Отдельной регистрации нет — кабинет создастся сам, если вы здесь впервые.</p>
|
||||
<div hidden="{{ otpSent }}" style="display: grid; gap: 10px;">
|
||||
<input type="email" placeholder="you@example.com" style="height: 50px; padding: 0 14px; border-radius: 12px; border: 1px solid var(--border); background: var(--bg); color: var(--ink); font-family: inherit; font-size: 15px; width: 100%;" />
|
||||
<button type="button" onClick="{{ sendOtp }}" style="height: 50px; border-radius: 12px; border: 0; background: var(--accent); color: var(--on-accent); font-family: inherit; font-size: 16px; font-weight: 500; cursor: pointer;" style-hover="background: var(--accent-hover);">Прислать код</button>
|
||||
</div>
|
||||
<div hidden="{{ otpNotSent }}" style="display: grid; gap: 10px;">
|
||||
<input type="text" inputmode="numeric" placeholder="6 цифр" style="height: 50px; padding: 0 14px; border-radius: 12px; border: 1px solid var(--border); background: var(--bg); color: var(--ink); font-family: inherit; font-size: 17px; letter-spacing: 0.28em; font-variant-numeric: tabular-nums; width: 100%;" />
|
||||
<button type="button" onClick="{{ signIn }}" style="height: 50px; border-radius: 12px; border: 0; background: var(--accent); color: var(--on-accent); font-family: inherit; font-size: 16px; font-weight: 500; cursor: pointer;" style-hover="background: var(--accent-hover);">Войти</button>
|
||||
<button type="button" onClick="{{ backToEmail }}" style="justify-self: start; border: 0; background: transparent; color: var(--ink-3); font-family: inherit; font-size: 14px; cursor: pointer; padding: 4px 0;">Другая почта</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer style="display: flex; flex-wrap: wrap; align-items: center; gap: 12px 20px; padding: 14px 24px; border-top: 1px solid var(--border); font-size: 13px; color: var(--ink-3);">
|
||||
<span>© 2026 ВейликсВПН</span>
|
||||
<a href="#legal" style="color: var(--ink-3);" style-hover="color: var(--ink);">Условия</a>
|
||||
<a href="#legal" style="color: var(--ink-3);" style-hover="color: var(--ink);">Конфиденциальность</a>
|
||||
<a href="MassSupport.dc.html" style="color: var(--ink-3);" style-hover="color: var(--ink);">Поддержка</a>
|
||||
<div style="margin-left: auto; display: flex; align-items: center; gap: 8px;">
|
||||
<div style="display: flex; width: max-content; padding: 3px; border-radius: 10px; border: 1px solid var(--border); background: var(--surface);">
|
||||
<button type="button" onClick="{{ setRu }}" style="padding: 6px 11px; border: 0; border-radius: 7px; background: {{ ruBg }}; color: {{ ruInk }}; font-family: inherit; font-size: 12px; cursor: pointer;">RU</button>
|
||||
<button type="button" onClick="{{ setEn }}" style="padding: 6px 11px; border: 0; border-radius: 7px; background: {{ enBg }}; color: {{ enInk }}; font-family: inherit; font-size: 12px; cursor: pointer;">EN</button>
|
||||
</div>
|
||||
<div style="display: flex; width: max-content; padding: 3px; border-radius: 10px; border: 1px solid var(--border); background: var(--surface);">
|
||||
<button type="button" onClick="{{ setLight }}" title="Светлая тема" style="display: flex; align-items: center; justify-content: center; width: 32px; height: 32px; border: 0; border-radius: 7px; background: {{ lightBg }}; color: {{ lightInk }}; cursor: pointer;">
|
||||
<span style="position: relative; width: 16px; height: 16px; display: block;">
|
||||
<span style="position: absolute; left: 3.5px; top: 3.5px; width: 9px; height: 9px; border-radius: 50%; background: currentColor; display: block;"></span>
|
||||
<span style="position: absolute; left: 0; top: 0; width: 16px; height: 16px; border-radius: 50%; border: 1.5px solid currentColor; opacity: 0.45; display: block;"></span>
|
||||
</span>
|
||||
</button>
|
||||
<button type="button" onClick="{{ setDark }}" title="Тёмная тема" style="display: flex; align-items: center; justify-content: center; width: 32px; height: 32px; border: 0; border-radius: 7px; background: {{ darkBg }}; color: {{ darkInk }}; cursor: pointer;">
|
||||
<span style="width: 16px; height: 16px; border-radius: 50%; box-shadow: inset -5px -2px 0 0 currentColor; display: block;"></span>
|
||||
</button>
|
||||
<button type="button" onClick="{{ setSystem }}" title="Как в системе" style="display: flex; align-items: center; justify-content: center; width: 32px; height: 32px; border: 0; border-radius: 7px; background: {{ systemBg }}; color: {{ systemInk }}; cursor: pointer;">
|
||||
<span style="width: 16px; height: 16px; border-radius: 50%; border: 1.5px solid currentColor; background: linear-gradient(90deg, currentColor 50%, transparent 50%); display: block;"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<div hidden="{{ loginOff }}" style="display: grid; grid-template-columns: {{ shellCols }}; min-height: 100vh;">
|
||||
|
||||
<div hidden="{{ topbarOff }}" style="position: sticky; top: 0; z-index: 30; display: flex; align-items: center; gap: 10px; padding: 12px 16px; background: color-mix(in oklab, var(--bg) 85%, transparent); backdrop-filter: blur(18px); -webkit-backdrop-filter: blur(18px); border-bottom: 1px solid var(--border);">
|
||||
<a href="MassMain.dc.html" style="display: flex; align-items: center; gap: 10px; color: var(--ink);">
|
||||
<span style="position: relative; width: 24px; height: 24px; display: block; flex: none;">
|
||||
<span style="position: absolute; left: 0; top: 3px; width: 18px; height: 18px; border-radius: 6px; background: linear-gradient(140deg, var(--accent), color-mix(in oklab, var(--accent) 55%, var(--glow-3))); display: block;"></span>
|
||||
<span style="position: absolute; right: 0; bottom: 3px; width: 18px; height: 18px; border-radius: 6px; background: var(--veil); border: 1px solid color-mix(in oklab, var(--accent) 35%, transparent); display: block;"></span>
|
||||
</span>
|
||||
<span style="font-family: 'Unbounded', 'Commissioner', sans-serif; font-weight: 600; font-size: 15px; letter-spacing: -0.02em;">ВейликсВПН</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<aside hidden="{{ asideOff }}" style="display: flex; flex-direction: column; gap: 24px; padding: 22px 16px; border-right: 1px solid var(--border); background: var(--surface-2); position: sticky; top: 0; height: 100vh;">
|
||||
<a href="MassMain.dc.html" style="display: flex; align-items: center; gap: 10px; padding: 4px 8px; color: var(--ink);">
|
||||
<span style="position: relative; width: 24px; height: 24px; display: block; flex: none;">
|
||||
<span style="position: absolute; left: 0; top: 3px; width: 18px; height: 18px; border-radius: 6px; background: linear-gradient(140deg, var(--accent), color-mix(in oklab, var(--accent) 55%, var(--glow-3))); display: block;"></span>
|
||||
<span style="position: absolute; right: 0; bottom: 3px; width: 18px; height: 18px; border-radius: 6px; background: var(--veil); border: 1px solid color-mix(in oklab, var(--accent) 35%, transparent); display: block;"></span>
|
||||
</span>
|
||||
<span style="font-family: 'Unbounded', 'Commissioner', sans-serif; font-weight: 600; font-size: 15px; letter-spacing: -0.02em;">ВейликсВПН</span>
|
||||
</a>
|
||||
|
||||
<nav style="display: grid; gap: 4px;">
|
||||
<button type="button" onClick="{{ goHome }}" style="display: flex; align-items: center; gap: 12px; padding: 11px 12px; border-radius: 12px; border: 0; background: {{ homeBg }}; color: {{ homeInk }}; font-family: inherit; font-size: 15px; text-align: left; cursor: pointer;" style-hover="background: var(--surface);">
|
||||
<span style="position: relative; width: 18px; height: 18px; flex: none; display: block;">
|
||||
<span style="position: absolute; left: 0; top: 0; width: 8px; height: 8px; border-radius: 2px; background: currentColor; display: block;"></span>
|
||||
<span style="position: absolute; right: 0; top: 0; width: 8px; height: 8px; border-radius: 2px; background: currentColor; opacity: 0.45; display: block;"></span>
|
||||
<span style="position: absolute; left: 0; bottom: 0; width: 8px; height: 8px; border-radius: 2px; background: currentColor; opacity: 0.45; display: block;"></span>
|
||||
<span style="position: absolute; right: 0; bottom: 0; width: 8px; height: 8px; border-radius: 2px; background: currentColor; display: block;"></span>
|
||||
</span>
|
||||
Главная
|
||||
</button>
|
||||
<button type="button" onClick="{{ goKeys }}" style="display: flex; align-items: center; gap: 12px; padding: 11px 12px; border-radius: 12px; border: 0; background: {{ keysBg }}; color: {{ keysInk }}; font-family: inherit; font-size: 15px; text-align: left; cursor: pointer;" style-hover="background: var(--surface);">
|
||||
<span style="position: relative; width: 18px; height: 18px; flex: none; display: block;">
|
||||
<span style="position: absolute; left: 0; top: 2px; width: 18px; height: 4px; border-radius: 2px; background: currentColor; display: block;"></span>
|
||||
<span style="position: absolute; left: 0; top: 8px; width: 18px; height: 4px; border-radius: 2px; background: currentColor; opacity: 0.6; display: block;"></span>
|
||||
<span style="position: absolute; left: 0; top: 14px; width: 18px; height: 4px; border-radius: 2px; background: currentColor; opacity: 0.3; display: block;"></span>
|
||||
</span>
|
||||
Подключения
|
||||
</button>
|
||||
<button type="button" onClick="{{ goBuy }}" style="display: flex; align-items: center; gap: 12px; padding: 11px 12px; border-radius: 12px; border: 0; background: {{ buyBg }}; color: {{ buyInk }}; font-family: inherit; font-size: 15px; text-align: left; cursor: pointer;" style-hover="background: var(--surface);">
|
||||
<span style="position: relative; width: 18px; height: 18px; flex: none; display: block;">
|
||||
<span style="position: absolute; left: 7px; top: 0; width: 4px; height: 18px; border-radius: 2px; background: currentColor; display: block;"></span>
|
||||
<span style="position: absolute; left: 0; top: 7px; width: 18px; height: 4px; border-radius: 2px; background: currentColor; display: block;"></span>
|
||||
</span>
|
||||
Выбрать страну
|
||||
</button>
|
||||
<button type="button" onClick="{{ goBilling }}" style="display: flex; align-items: center; gap: 12px; padding: 11px 12px; border-radius: 12px; border: 0; background: {{ billingBg }}; color: {{ billingInk }}; font-family: inherit; font-size: 15px; text-align: left; cursor: pointer;" style-hover="background: var(--surface);">
|
||||
<span style="position: relative; width: 18px; height: 18px; flex: none; display: block;">
|
||||
<span style="position: absolute; left: 0; top: 2px; width: 18px; height: 14px; border-radius: 4px; border: 2px solid currentColor; display: block;"></span>
|
||||
<span style="position: absolute; left: 0; top: 6px; width: 18px; height: 3px; background: currentColor; display: block;"></span>
|
||||
</span>
|
||||
Счёт и платежи
|
||||
</button>
|
||||
<button type="button" onClick="{{ goRef }}" style="display: flex; align-items: center; gap: 12px; padding: 11px 12px; border-radius: 12px; border: 0; background: {{ refBg }}; color: {{ refInk }}; font-family: inherit; font-size: 15px; text-align: left; cursor: pointer;" style-hover="background: var(--surface);">
|
||||
<span style="position: relative; width: 18px; height: 18px; flex: none; display: block;">
|
||||
<span style="position: absolute; left: 0; top: 5px; width: 8px; height: 8px; border-radius: 50%; border: 2px solid currentColor; display: block;"></span>
|
||||
<span style="position: absolute; right: 0; top: 0; width: 8px; height: 8px; border-radius: 50%; background: currentColor; display: block;"></span>
|
||||
<span style="position: absolute; right: 2px; bottom: 0; width: 8px; height: 8px; border-radius: 50%; background: currentColor; opacity: 0.45; display: block;"></span>
|
||||
</span>
|
||||
Реферальная программа
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
<div style="margin-top: auto; display: grid; gap: 14px; padding: 0 4px;">
|
||||
<a href="MassSupport.dc.html" style="display: flex; align-items: center; gap: 12px; padding: 11px 8px; border-radius: 12px; color: var(--ink-2); font-size: 15px;" style-hover="background: var(--surface); color: var(--ink);">
|
||||
<span style="position: relative; width: 18px; height: 18px; flex: none; display: block;">
|
||||
<span style="position: absolute; left: 1px; top: 1px; width: 16px; height: 16px; border-radius: 8px 8px 0 0; border: 2px solid currentColor; border-bottom: 0; display: block;"></span>
|
||||
<span style="position: absolute; left: 0; top: 8px; width: 5px; height: 9px; border-radius: 2px; background: currentColor; display: block;"></span>
|
||||
<span style="position: absolute; right: 0; top: 8px; width: 5px; height: 9px; border-radius: 2px; background: currentColor; display: block;"></span>
|
||||
</span>
|
||||
Поддержка
|
||||
</a>
|
||||
<div>
|
||||
<div style="font-size: 12px; color: var(--ink-3); margin-bottom: 6px; padding: 0 4px;">Тема</div>
|
||||
<div style="display: flex; flex-wrap: wrap; align-items: center; gap: 6px; width: max-content; max-width: 100%; align-self: start; padding: 3px; border-radius: 12px; border: 1px solid var(--border); background: var(--surface);">
|
||||
<button type="button" onClick="{{ setLight }}" title="Светлая тема" style="display: flex; align-items: center; justify-content: center; width: 36px; height: 36px; border: 0; border-radius: 9px; background: {{ lightBg }}; color: {{ lightInk }}; cursor: pointer;">
|
||||
<span style="position: relative; width: 18px; height: 18px; display: block;">
|
||||
<span style="position: absolute; left: 4px; top: 4px; width: 10px; height: 10px; border-radius: 50%; background: currentColor; display: block;"></span>
|
||||
<span style="position: absolute; left: 0; top: 0; width: 18px; height: 18px; border-radius: 50%; border: 1.5px solid currentColor; opacity: 0.45; display: block;"></span>
|
||||
</span>
|
||||
</button>
|
||||
<button type="button" onClick="{{ setDark }}" title="Тёмная тема" style="display: flex; align-items: center; justify-content: center; width: 36px; height: 36px; border: 0; border-radius: 9px; background: {{ darkBg }}; color: {{ darkInk }}; cursor: pointer;">
|
||||
<span style="width: 18px; height: 18px; border-radius: 50%; box-shadow: inset -6px -2px 0 0 currentColor; display: block;"></span>
|
||||
</button>
|
||||
<button type="button" onClick="{{ setSystem }}" title="Как в системе" style="display: flex; align-items: center; justify-content: center; width: 36px; height: 36px; border: 0; border-radius: 9px; background: {{ systemBg }}; color: {{ systemInk }}; cursor: pointer;">
|
||||
<span style="width: 18px; height: 18px; border-radius: 50%; border: 1.5px solid currentColor; background: linear-gradient(90deg, currentColor 50%, transparent 50%); display: block;"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size: 12px; color: var(--ink-3); margin-bottom: 6px; padding: 0 4px;">Язык</div>
|
||||
<div style="display: flex; flex-wrap: wrap; width: max-content; max-width: 100%; align-self: start; padding: 3px; border-radius: 12px; border: 1px solid var(--border); background: var(--surface);">
|
||||
<button type="button" onClick="{{ setRu }}" style="padding: 8px 14px; border: 0; border-radius: 9px; background: {{ ruBg }}; color: {{ ruInk }}; font-family: inherit; font-size: 13px; cursor: pointer;">RU</button>
|
||||
<button type="button" onClick="{{ setEn }}" style="padding: 8px 14px; border: 0; border-radius: 9px; background: {{ enBg }}; color: {{ enInk }}; font-family: inherit; font-size: 13px; cursor: pointer;">EN</button>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" onClick="{{ signOut }}" style="justify-self: start; border: 0; background: transparent; color: var(--ink-3); font-family: inherit; font-size: 13px; cursor: pointer; padding: 4px 8px;">Выйти · anna@example.com</button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<main style="padding: {{ mainPad }}; max-width: 960px;">
|
||||
|
||||
<div hidden="{{ homeOff }}" style="display: grid; gap: 28px;">
|
||||
<div>
|
||||
<h1 style="margin: 0 0 6px; font-size: 30px; font-weight: 600; letter-spacing: -0.03em;">Ваша подписка</h1>
|
||||
<p style="margin: 0; font-size: 15px; color: var(--ink-2); font-weight: 300;">Тридцать стран и российский адрес, до пяти устройств.</p>
|
||||
</div>
|
||||
|
||||
<div style="display: grid; gap: 12px;">
|
||||
<sc-for list="{{ keys }}" as="key" hint-placeholder-count="2">
|
||||
<button type="button" onClick="{{ key.open }}" style="display: grid; gap: 16px; padding: 24px; border-radius: 20px; border: 1px solid var(--border); background: var(--surface); box-shadow: var(--shadow); color: var(--ink); font-family: inherit; text-align: left; cursor: pointer; width: 100%;" style-hover="border-color: var(--accent);">
|
||||
<span style="display: flex; flex-wrap: wrap; align-items: center; gap: 12px;">
|
||||
<span style="font-size: 19px; font-weight: 500; letter-spacing: -0.02em;">{{ key.title }}</span>
|
||||
<span style="padding: 4px 10px; border-radius: 8px; background: {{ key.badgeBg }}; color: {{ key.badgeInk }}; font-size: 12px;">{{ key.badge }}</span>
|
||||
<span style="margin-left: auto; font-size: 14px; color: var(--ink-3); font-variant-numeric: tabular-nums;">{{ key.ip }}</span>
|
||||
</span>
|
||||
<span style="display: flex; flex-wrap: wrap; gap: 8px 28px; font-size: 14px; color: var(--ink-2);">
|
||||
<span>Действует до {{ key.until }}</span>
|
||||
<span>{{ key.devices }}</span>
|
||||
<span>{{ key.payment }}</span>
|
||||
</span>
|
||||
</button>
|
||||
</sc-for>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 16px; padding: 24px; border-radius: 20px; background: var(--surface-2); border: 1px dashed var(--border);">
|
||||
<div style="display: grid; gap: 4px;">
|
||||
<div style="font-size: 17px; font-weight: 500;">Хотите другую страну</div>
|
||||
<div style="font-size: 14px; color: var(--ink-2); font-weight: 300;">Все локации уже в подписке, переключение бесплатное.</div>
|
||||
</div>
|
||||
<button type="button" onClick="{{ goBuy }}" style="height: 46px; padding: 0 22px; border-radius: 12px; border: 0; background: var(--accent); color: var(--on-accent); font-family: inherit; font-size: 15px; font-weight: 500; cursor: pointer;" style-hover="background: var(--accent-hover);">Сменить страну</button>
|
||||
</div>
|
||||
|
||||
<div style="display: grid; gap: 10px;">
|
||||
<h2 style="margin: 0 0 4px; font-size: 20px; font-weight: 600; letter-spacing: -0.025em;">Короткие ответы</h2>
|
||||
<sc-for list="{{ faq }}" as="item" hint-placeholder-count="3">
|
||||
<div style="border: 1px solid var(--border); border-radius: 16px; background: var(--surface); overflow: hidden;">
|
||||
<button type="button" onClick="{{ item.toggle }}" style="display: flex; align-items: center; gap: 16px; width: 100%; padding: 17px 20px; background: transparent; border: 0; color: var(--ink); font-family: inherit; font-size: 16px; font-weight: 500; text-align: left; cursor: pointer;">
|
||||
<span style="flex: 1;">{{ item.q }}</span>
|
||||
<span style="flex: none; width: 9px; height: 9px; border-right: 1.5px solid var(--ink-3); border-bottom: 1.5px solid var(--ink-3); transform: {{ item.rot }}; transition: transform 180ms ease; display: block;"></span>
|
||||
</button>
|
||||
<div hidden="{{ item.off }}" style="padding: 0 20px 20px; font-size: 15px; line-height: 1.6; color: var(--ink-2); font-weight: 300; max-width: 62ch;">{{ item.a }}</div>
|
||||
</div>
|
||||
</sc-for>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div hidden="{{ keysOff }}" style="display: grid; gap: 20px;">
|
||||
<h1 style="margin: 0; font-size: 30px; font-weight: 600; letter-spacing: -0.03em;">Подключения</h1>
|
||||
<div style="display: grid; gap: 12px;">
|
||||
<sc-for list="{{ allKeys }}" as="key" hint-placeholder-count="3">
|
||||
<button type="button" onClick="{{ key.open }}" style="display: flex; flex-wrap: wrap; align-items: center; gap: 12px 20px; padding: 20px 24px; border-radius: 18px; border: 1px solid var(--border); background: var(--surface); color: var(--ink); font-family: inherit; text-align: left; cursor: pointer; width: 100%;" style-hover="border-color: var(--accent);">
|
||||
<span style="font-size: 17px; font-weight: 500;">{{ key.title }}</span>
|
||||
<span style="padding: 4px 10px; border-radius: 8px; background: {{ key.badgeBg }}; color: {{ key.badgeInk }}; font-size: 12px;">{{ key.badge }}</span>
|
||||
<span style="margin-left: auto; font-size: 14px; color: var(--ink-3); font-variant-numeric: tabular-nums;">{{ key.ip }}</span>
|
||||
<span style="font-size: 14px; color: var(--ink-2); font-variant-numeric: tabular-nums;">{{ key.untilLine }}</span>
|
||||
</button>
|
||||
</sc-for>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div hidden="{{ keyOff }}" style="display: grid; gap: 22px;">
|
||||
<button type="button" onClick="{{ goKeys }}" style="justify-self: start; border: 0; background: transparent; color: var(--ink-3); font-family: inherit; font-size: 14px; cursor: pointer; padding: 0;">← Все подключения</button>
|
||||
|
||||
<div style="display: flex; flex-wrap: wrap; align-items: baseline; gap: 12px 20px;">
|
||||
<h1 style="margin: 0; font-size: 30px; font-weight: 600; letter-spacing: -0.03em;">{{ cur.title }}</h1>
|
||||
<span style="font-size: 15px; color: var(--ink-3); font-variant-numeric: tabular-nums;">{{ cur.ip }}</span>
|
||||
</div>
|
||||
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 12px;">
|
||||
<div style="padding: 22px; border-radius: 18px; background: var(--surface); border: 1px solid var(--border); box-shadow: var(--shadow);">
|
||||
<div style="font-size: 13px; color: var(--ink-3); margin-bottom: 10px;">Действует до</div>
|
||||
<div style="font-size: 24px; font-weight: 600; letter-spacing: -0.025em;">{{ cur.until }}</div>
|
||||
<div style="margin-top: 8px; font-size: 14px; color: {{ cur.leftInk }};">{{ cur.left }}</div>
|
||||
</div>
|
||||
<div style="padding: 22px; border-radius: 18px; background: var(--surface); border: 1px solid var(--border); box-shadow: var(--shadow);">
|
||||
<div style="font-size: 13px; color: var(--ink-3); margin-bottom: 10px;">Оплата</div>
|
||||
<div style="font-size: 18px; font-variant-numeric: tabular-nums;">{{ cur.card }}</div>
|
||||
<div style="margin-top: 8px; font-size: 14px; color: var(--ink-2);">699 ₽ в месяц · карта аккаунта</div>
|
||||
<button type="button" onClick="{{ goBilling }}" style="margin-top: 14px; height: 40px; padding: 0 16px; border-radius: 10px; border: 1px solid var(--border); background: transparent; color: var(--ink); font-family: inherit; font-size: 14px; cursor: pointer;" style-hover="border-color: var(--ink-3);">Счёт и платежи</button>
|
||||
</div>
|
||||
<div style="padding: 22px; border-radius: 18px; background: var(--surface); border: 1px solid var(--border); box-shadow: var(--shadow);">
|
||||
<div style="font-size: 13px; color: var(--ink-3); margin-bottom: 10px;">Устройства</div>
|
||||
<div style="font-size: 24px; font-weight: 600; letter-spacing: -0.025em;">{{ cur.devices }}</div>
|
||||
<div style="margin-top: 8px; font-size: 14px; color: var(--ink-2);">Каждое в своей стране</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="padding: 26px; border-radius: 20px; background: var(--surface); border: 1px solid var(--border); box-shadow: var(--shadow);">
|
||||
<div style="display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; gap: 12px; margin-bottom: 6px;">
|
||||
<h2 style="margin: 0; font-size: 20px; font-weight: 600; letter-spacing: -0.025em;">Конфигурации XRAY</h2>
|
||||
<span style="font-size: 13px; color: var(--ink-3);">5 конфигураций — по одной на устройство</span>
|
||||
</div>
|
||||
<p style="margin: 0 0 18px; font-size: 14px; color: var(--ink-2); font-weight: 300; max-width: 60ch;">На компьютере скачайте файл и откройте его в клиенте XRAY. На телефоне — отсканируйте QR-код.</p>
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(210px, 1fr)); gap: 10px;">
|
||||
<sc-for list="{{ configs }}" as="cfg" hint-placeholder-count="5">
|
||||
<div style="display: grid; gap: 12px; padding: 16px; border-radius: 14px; background: var(--surface-2); border: 1px solid var(--border);">
|
||||
<div style="display: flex; align-items: center; justify-content: space-between; gap: 10px;">
|
||||
<span style="font-size: 15px; font-weight: 500;">{{ cfg.name }}</span>
|
||||
<span style="font-size: 12px; color: var(--ink-3);">{{ cfg.state }}</span>
|
||||
</div>
|
||||
<div style="display: flex; gap: 8px;">
|
||||
<button type="button" style="flex: 1; height: 38px; border-radius: 10px; border: 0; background: var(--accent); color: var(--on-accent); font-family: inherit; font-size: 14px; cursor: pointer;" style-hover="background: var(--accent-hover);">Скачать</button>
|
||||
<button type="button" onClick="{{ cfg.showQr }}" style="height: 38px; padding: 0 14px; border-radius: 10px; border: 1px solid var(--border); background: var(--bg); color: var(--ink); font-family: inherit; font-size: 14px; cursor: pointer;" style-hover="border-color: var(--ink-3);">QR</button>
|
||||
</div>
|
||||
</div>
|
||||
</sc-for>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="padding: 26px; border-radius: 20px; background: var(--surface); border: 1px solid var(--border); box-shadow: var(--shadow);">
|
||||
<div style="display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; gap: 12px; margin-bottom: 6px;">
|
||||
<h2 style="margin: 0; font-size: 20px; font-weight: 600; letter-spacing: -0.025em;">Ссылка подключения</h2>
|
||||
<span style="font-size: 13px; color: var(--ink-3);">Для Happ и других клиентов XRAY</span>
|
||||
</div>
|
||||
<p style="margin: 0 0 18px; font-size: 14px; color: var(--ink-2); font-weight: 300; max-width: 60ch;">Быстрый способ подключить новое устройство. Скопируйте ссылку или отсканируйте код — настройки те же.</p>
|
||||
<div style="display: flex; flex-wrap: wrap; gap: 10px; align-items: center;">
|
||||
<div style="flex: 1; min-width: 240px; padding: 14px 16px; border-radius: 12px; background: var(--surface-2); border: 1px solid var(--border); font-size: 13px; color: var(--ink-2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-variant-numeric: tabular-nums;">{{ cur.xray }}</div>
|
||||
<button type="button" onClick="{{ copyXray }}" style="height: 46px; padding: 0 20px; border-radius: 12px; border: 0; background: var(--accent); color: var(--on-accent); font-family: inherit; font-size: 15px; cursor: pointer;" style-hover="background: var(--accent-hover);">{{ copyLabel }}</button>
|
||||
<button type="button" onClick="{{ showXrayQr }}" style="height: 46px; padding: 0 18px; border-radius: 12px; border: 1px solid var(--border); background: transparent; color: var(--ink); font-family: inherit; font-size: 15px; cursor: pointer;" style-hover="border-color: var(--ink-3);">QR</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div hidden="{{ buyOff }}" style="display: grid; gap: 22px;">
|
||||
<div>
|
||||
<h1 style="margin: 0 0 6px; font-size: 30px; font-weight: 600; letter-spacing: -0.03em;">Выбрать страну</h1>
|
||||
<p style="margin: 0; font-size: 15px; color: var(--ink-2); font-weight: 300;">Все тридцать локаций входят в подписку. Переключение бесплатное и занимает пару секунд.</p>
|
||||
</div>
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 10px;">
|
||||
<sc-for list="{{ locations }}" as="loc" hint-placeholder-count="12">
|
||||
<div role="button" tabindex="0" onClick="{{ loc.pick }}" style="display: grid; align-content: start; gap: 14px; padding: 20px; border-radius: 18px; border: 1px solid {{ loc.border }}; background: {{ loc.bg }}; color: var(--ink); cursor: pointer;" style-hover="border-color: var(--ink-3);">
|
||||
<span style="display: flex; align-items: center; justify-content: space-between; gap: 14px;">
|
||||
{{ loc.flag }}
|
||||
<span style="display: flex; align-items: center; gap: 12px; flex: none; min-height: 32px;">
|
||||
<span hidden="{{ loc.buyOff }}" style="display: flex; align-items: center; justify-content: center; height: 32px; padding: 0 16px; border-radius: 10px; background: var(--accent); color: var(--on-accent); font-size: 14px; font-weight: 500; cursor: pointer;">Выбрать</span>
|
||||
</span>
|
||||
</span>
|
||||
<span style="display: grid; gap: 5px;">
|
||||
<span style="font-size: 17px; font-weight: 500; letter-spacing: -0.02em;">{{ loc.country }}</span>
|
||||
<span style="font-size: 14px; color: var(--ink-3);">{{ loc.city }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</sc-for>
|
||||
</div>
|
||||
<div style="padding: 24px; border-radius: 18px; background: var(--surface); border: 1px solid var(--border);">
|
||||
<h2 id="lead" style="margin: 0 0 8px; font-size: 19px; font-weight: 600; letter-spacing: -0.02em; scroll-margin-top: 24px;">Нужны особые условия</h2>
|
||||
<p style="margin: 0 0 18px; max-width: 60ch; font-size: 14px; line-height: 1.55; color: var(--ink-2); font-weight: 300;">Подписки на команду, оплата по счёту или редкая страна. Оставьте заявку — ответим в течение дня.</p>
|
||||
<button type="button" onClick="{{ openLead }}" hidden="{{ leadOpen }}" style="display: inline-flex; align-items: center; height: 46px; padding: 0 20px; border-radius: 12px; border: 1px solid var(--accent); background: transparent; color: var(--accent); font-family: inherit; font-size: 15px; font-weight: 500; cursor: pointer;" style-hover="background: var(--accent-soft);">Оставить заявку</button>
|
||||
<div hidden="{{ leadClosed }}" style="display: grid; gap: 10px; max-width: 460px;">
|
||||
<input type="text" placeholder="Имя" style="height: 48px; padding: 0 14px; border-radius: 12px; border: 1px solid var(--border); background: var(--bg); color: var(--ink); font-family: inherit; font-size: 15px; width: 100%;" />
|
||||
<input type="email" placeholder="Email" style="height: 48px; padding: 0 14px; border-radius: 12px; border: 1px solid var(--border); background: var(--bg); color: var(--ink); font-family: inherit; font-size: 15px; width: 100%;" />
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 10px;">
|
||||
<input type="text" placeholder="Telegram" style="height: 48px; padding: 0 14px; border-radius: 12px; border: 1px solid var(--border); background: var(--bg); color: var(--ink); font-family: inherit; font-size: 15px; width: 100%;" />
|
||||
<input type="text" placeholder="WhatsApp" style="height: 48px; padding: 0 14px; border-radius: 12px; border: 1px solid var(--border); background: var(--bg); color: var(--ink); font-family: inherit; font-size: 15px; width: 100%;" />
|
||||
</div>
|
||||
<textarea rows="3" placeholder="Коротко о задаче" style="padding: 12px 14px; border-radius: 12px; border: 1px solid var(--border); background: var(--bg); color: var(--ink); font-family: inherit; font-size: 15px; resize: vertical; width: 100%;"></textarea>
|
||||
<button type="button" onClick="{{ sendLead }}" style="display: inline-flex; align-items: center; justify-content: center; height: 48px; padding: 0 22px; border-radius: 12px; border: 0; background: var(--accent); color: var(--on-accent); font-family: inherit; font-size: 15px; font-weight: 500; cursor: pointer;" style-hover="background: var(--accent-hover);">{{ leadButton }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div hidden="{{ billingOff }}" style="display: grid; gap: 22px;">
|
||||
<div>
|
||||
<h1 style="margin: 0 0 6px; font-size: 30px; font-weight: 600; letter-spacing: -0.03em;">Счёт и платежи</h1>
|
||||
<p style="margin: 0; max-width: 62ch; font-size: 15px; line-height: 1.55; color: var(--ink-2); font-weight: 300;">Карта привязана к аккаунту. Подписка списывается с неё — или с баланса, если на нём есть деньги.</p>
|
||||
</div>
|
||||
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); gap: 12px;">
|
||||
<div style="display: grid; align-content: start; gap: 8px; padding: 22px; border-radius: 18px; background: var(--surface); border: 1px solid var(--border); box-shadow: var(--shadow);">
|
||||
<div style="font-size: 13px; color: var(--ink-3);">Баланс</div>
|
||||
<div style="font-size: 30px; font-weight: 600; letter-spacing: -0.03em; font-variant-numeric: tabular-nums;">1 250 ₽</div>
|
||||
<div style="font-size: 14px; color: var(--ink-2);">Списывается в первую очередь</div>
|
||||
<button type="button" onClick="{{ openTopup }}" style="justify-self: start; margin-top: 6px; height: 40px; padding: 0 16px; border-radius: 10px; border: 0; background: var(--accent); color: var(--on-accent); font-family: inherit; font-size: 14px; font-weight: 500; cursor: pointer;" style-hover="background: var(--accent-hover);">Пополнить</button>
|
||||
</div>
|
||||
<div style="display: grid; align-content: start; gap: 8px; padding: 22px; border-radius: 18px; background: var(--surface); border: 1px solid var(--border); box-shadow: var(--shadow);">
|
||||
<div style="font-size: 13px; color: var(--ink-3);">Способ оплаты</div>
|
||||
<div style="font-size: 20px; font-weight: 500; font-variant-numeric: tabular-nums;">Карта ···· 4242</div>
|
||||
<div style="font-size: 14px; color: var(--ink-2);">Основная для аккаунта</div>
|
||||
<button type="button" style="justify-self: start; margin-top: 6px; height: 40px; padding: 0 16px; border-radius: 10px; border: 1px solid var(--border); background: transparent; color: var(--ink); font-family: inherit; font-size: 14px; cursor: pointer;" style-hover="border-color: var(--ink-3);">Изменить карту</button>
|
||||
</div>
|
||||
<div style="display: grid; align-content: start; gap: 8px; padding: 22px; border-radius: 18px; background: var(--surface); border: 1px solid var(--border); box-shadow: var(--shadow);">
|
||||
<div style="font-size: 13px; color: var(--ink-3);">Следующее списание</div>
|
||||
<div style="font-size: 30px; font-weight: 600; letter-spacing: -0.03em; font-variant-numeric: tabular-nums;">699 ₽</div>
|
||||
<div style="font-size: 14px; color: var(--ink-2);">14 октября · подписка на месяц</div>
|
||||
<div style="font-size: 13px; color: var(--ink-3);">Год выгоднее: 6 990 ₽ за 12 месяцев</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div hidden="{{ topupOff }}" style="display: grid; gap: 16px; padding: 26px; border-radius: 20px; background: var(--surface); border: 1px solid var(--border); box-shadow: var(--shadow);">
|
||||
<div style="display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; gap: 12px;">
|
||||
<h2 style="margin: 0; font-size: 20px; font-weight: 600; letter-spacing: -0.025em;">Пополнить баланс</h2>
|
||||
<button type="button" onClick="{{ closeTopup }}" style="border: 0; background: transparent; color: var(--ink-3); font-family: inherit; font-size: 14px; cursor: pointer; padding: 0;">Свернуть</button>
|
||||
</div>
|
||||
<div style="display: grid; gap: 8px;">
|
||||
<div style="font-size: 13px; color: var(--ink-3);">Сумма</div>
|
||||
<div style="display: flex; flex-wrap: wrap; gap: 8px;">
|
||||
<sc-for list="{{ sums }}" as="sum" hint-placeholder-count="4">
|
||||
<button type="button" onClick="{{ sum.pick }}" style="height: 44px; padding: 0 18px; border-radius: 12px; border: 1px solid {{ sum.border }}; background: {{ sum.bg }}; color: var(--ink); font-family: inherit; font-size: 15px; font-variant-numeric: tabular-nums; cursor: pointer;">{{ sum.label }}</button>
|
||||
</sc-for>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: grid; gap: 8px;">
|
||||
<div style="font-size: 13px; color: var(--ink-3);">Способ</div>
|
||||
<div style="display: flex; flex-wrap: wrap; gap: 8px;">
|
||||
<sc-for list="{{ methods }}" as="m" hint-placeholder-count="3">
|
||||
<button type="button" onClick="{{ m.pick }}" style="height: 44px; padding: 0 18px; border-radius: 12px; border: 1px solid {{ m.border }}; background: {{ m.bg }}; color: var(--ink); font-family: inherit; font-size: 15px; cursor: pointer;">{{ m.label }}</button>
|
||||
</sc-for>
|
||||
</div>
|
||||
</div>
|
||||
<div hidden="{{ cryptoOff }}" style="display: grid; gap: 10px; padding: 18px; border-radius: 14px; background: var(--surface-2); border: 1px solid var(--border);">
|
||||
<div style="font-size: 15px; font-weight: 500;">USDT (TRC-20), BTC, ETH</div>
|
||||
<p style="margin: 0; max-width: 60ch; font-size: 14px; line-height: 1.55; color: var(--ink-2); font-weight: 300;">Покажем адрес и сумму в выбранной монете. Курс фиксируется на 15 минут, баланс пополнится после одного подтверждения сети.</p>
|
||||
</div>
|
||||
<button type="button" onClick="{{ sendTopup }}" style="justify-self: start; height: 48px; padding: 0 22px; border-radius: 12px; border: 0; background: var(--accent); color: var(--on-accent); font-family: inherit; font-size: 15px; font-weight: 500; cursor: pointer;" style-hover="background: var(--accent-hover);">{{ topupButton }}</button>
|
||||
</div>
|
||||
|
||||
<div style="display: grid; gap: 10px;">
|
||||
<h2 style="margin: 0 0 4px; font-size: 20px; font-weight: 600; letter-spacing: -0.025em;">История платежей</h2>
|
||||
<sc-for list="{{ payments }}" as="p" hint-placeholder-count="5">
|
||||
<div style="display: flex; flex-wrap: wrap; align-items: center; gap: 10px 20px; padding: 18px 20px; border-radius: 16px; background: var(--surface); border: 1px solid var(--border);">
|
||||
<span style="width: 116px; flex: none; font-size: 14px; color: var(--ink-3); font-variant-numeric: tabular-nums;">{{ p.date }}</span>
|
||||
<span style="flex: 1; min-width: 180px; display: grid; gap: 3px;">
|
||||
<span style="font-size: 15px;">{{ p.title }}</span>
|
||||
<span style="font-size: 13px; color: var(--ink-3);">{{ p.method }}</span>
|
||||
</span>
|
||||
<span style="font-size: 13px; color: var(--ink-3);">{{ p.status }}</span>
|
||||
<span style="width: 104px; flex: none; text-align: right; font-size: 16px; font-weight: 500; color: {{ p.ink }}; font-variant-numeric: tabular-nums;">{{ p.amount }}</span>
|
||||
</div>
|
||||
</sc-for>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div hidden="{{ refOff }}" style="display: grid; gap: 20px;">
|
||||
<div>
|
||||
<h1 style="margin: 0 0 6px; font-size: 30px; font-weight: 600; letter-spacing: -0.03em;">Реферальная программа</h1>
|
||||
<p style="margin: 0; max-width: 62ch; font-size: 15px; line-height: 1.55; color: var(--ink-2); font-weight: 300;">Приведите друга — вы оба получите по месяцу в подарок.</p>
|
||||
</div>
|
||||
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 12px;">
|
||||
<div style="padding: 22px; border-radius: 18px; background: var(--surface); border: 1px solid var(--border); box-shadow: var(--shadow);">
|
||||
<div style="font-size: 13px; color: var(--ink-3); margin-bottom: 10px;">Приглашено</div>
|
||||
<div style="font-size: 30px; font-weight: 600; letter-spacing: -0.03em; font-variant-numeric: tabular-nums;">3</div>
|
||||
</div>
|
||||
<div style="padding: 22px; border-radius: 18px; background: var(--surface); border: 1px solid var(--border); box-shadow: var(--shadow);">
|
||||
<div style="font-size: 13px; color: var(--ink-3); margin-bottom: 10px;">Месяцев начислено</div>
|
||||
<div style="font-size: 30px; font-weight: 600; letter-spacing: -0.03em; font-variant-numeric: tabular-nums;">2</div>
|
||||
</div>
|
||||
<div style="padding: 22px; border-radius: 18px; background: var(--surface); border: 1px solid var(--border); box-shadow: var(--shadow);">
|
||||
<div style="font-size: 13px; color: var(--ink-3); margin-bottom: 10px;">Ожидает подтверждения</div>
|
||||
<div style="font-size: 30px; font-weight: 600; letter-spacing: -0.03em; font-variant-numeric: tabular-nums;">1</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display: grid; gap: 14px; padding: 26px; border-radius: 20px; background: var(--surface); border: 1px solid var(--border); box-shadow: var(--shadow);">
|
||||
<div style="font-size: 16px; font-weight: 500;">Ваша ссылка</div>
|
||||
<div style="display: flex; flex-wrap: wrap; gap: 10px; align-items: center;">
|
||||
<div style="flex: 1; min-width: 220px; padding: 14px 16px; border-radius: 12px; background: var(--surface-2); border: 1px solid var(--border); font-size: 14px; color: var(--ink-2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">veilix.com/r/anna-7f2c</div>
|
||||
<button type="button" onClick="{{ copyRef }}" style="height: 46px; padding: 0 20px; border-radius: 12px; border: 0; background: var(--accent); color: var(--on-accent); font-family: inherit; font-size: 15px; cursor: pointer;" style-hover="background: var(--accent-hover);">{{ refLabel }}</button>
|
||||
</div>
|
||||
<div style="font-size: 13px; color: var(--ink-3); line-height: 1.5;">Месяц начисляется вам и другу после его первой оплаты и применяется к ближайшему списанию.</div>
|
||||
</div>
|
||||
|
||||
<div style="display: grid; gap: 10px;">
|
||||
<div style="font-size: 16px; font-weight: 500;">Приглашённые</div>
|
||||
<div style="display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 12px; padding: 16px 20px; border-radius: 16px; background: var(--surface); border: 1px solid var(--border);">
|
||||
<span style="font-size: 15px;">m···@gmail.com</span>
|
||||
<span style="font-size: 14px; color: var(--accent);">Месяц начислен</span>
|
||||
</div>
|
||||
<div style="display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 12px; padding: 16px 20px; border-radius: 16px; background: var(--surface); border: 1px solid var(--border);">
|
||||
<span style="font-size: 15px;">d···@yandex.ru</span>
|
||||
<span style="font-size: 14px; color: var(--accent);">Месяц начислен</span>
|
||||
</div>
|
||||
<div style="display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 12px; padding: 16px 20px; border-radius: 16px; background: var(--surface); border: 1px solid var(--border);">
|
||||
<span style="font-size: 15px;">k···@icloud.com</span>
|
||||
<span style="font-size: 14px; color: var(--ink-3);">Ждём оплаты</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div hidden="{{ moreOff }}" style="display: grid; gap: 16px;">
|
||||
<h1 style="margin: 0; font-size: 26px; font-weight: 600; letter-spacing: -0.03em;">Ещё</h1>
|
||||
<button type="button" onClick="{{ goBilling }}" style="display: flex; align-items: center; justify-content: space-between; gap: 12px; min-height: 56px; padding: 14px 18px; border-radius: 16px; border: 1px solid var(--border); background: var(--surface); color: var(--ink); font-family: inherit; font-size: 16px; text-align: left; cursor: pointer;">
|
||||
<span>Счёт и платежи</span>
|
||||
<span style="color: var(--ink-3);">›</span>
|
||||
</button>
|
||||
<button type="button" onClick="{{ goRef }}" style="display: flex; align-items: center; justify-content: space-between; gap: 12px; min-height: 56px; padding: 14px 18px; border-radius: 16px; border: 1px solid var(--border); background: var(--surface); color: var(--ink); font-family: inherit; font-size: 16px; text-align: left; cursor: pointer;">
|
||||
<span>Реферальная программа</span>
|
||||
<span style="color: var(--ink-3);">›</span>
|
||||
</button>
|
||||
<a href="MassSupport.dc.html" style="display: flex; align-items: center; justify-content: space-between; gap: 12px; min-height: 56px; padding: 14px 18px; border-radius: 16px; border: 1px solid var(--border); background: var(--surface); color: var(--ink); font-size: 16px;">
|
||||
<span>Поддержка</span>
|
||||
<span style="color: var(--ink-3);">›</span>
|
||||
</a>
|
||||
<div style="display: grid; gap: 12px; padding: 14px 18px; border-radius: 16px; border: 1px solid var(--border); background: var(--surface);">
|
||||
<div style="display: flex; align-items: center; justify-content: space-between; gap: 12px;">
|
||||
<div style="font-size: 15px;">Тема</div>
|
||||
<div style="display: flex; padding: 3px; border-radius: 11px; border: 1px solid var(--border); flex: none;">
|
||||
<button type="button" onClick="{{ setLight }}" title="Светлая тема" style="display: flex; align-items: center; justify-content: center; width: 40px; height: 34px; border: 0; border-radius: 8px; background: {{ lightBg }}; color: var(--ink); cursor: pointer;">
|
||||
<span style="position: relative; width: 16px; height: 16px; display: block;">
|
||||
<span style="position: absolute; left: 4px; top: 4px; width: 8px; height: 8px; border-radius: 50%; background: currentColor; display: block;"></span>
|
||||
<span style="position: absolute; left: 0; top: 0; width: 16px; height: 16px; border-radius: 50%; border: 1.5px solid currentColor; opacity: 0.45; display: block;"></span>
|
||||
</span>
|
||||
</button>
|
||||
<button type="button" onClick="{{ setDark }}" title="Тёмная тема" style="display: flex; align-items: center; justify-content: center; width: 40px; height: 34px; border: 0; border-radius: 8px; background: {{ darkBg }}; color: var(--ink); cursor: pointer;">
|
||||
<span style="width: 16px; height: 16px; border-radius: 50%; box-shadow: inset -5px -2px 0 0 currentColor; display: block;"></span>
|
||||
</button>
|
||||
<button type="button" onClick="{{ setSystem }}" title="Как в системе" style="display: flex; align-items: center; justify-content: center; width: 40px; height: 34px; border: 0; border-radius: 8px; background: {{ systemBg }}; color: var(--ink); cursor: pointer;">
|
||||
<span style="width: 16px; height: 16px; border-radius: 50%; border: 1.5px solid currentColor; background: linear-gradient(90deg, currentColor 50%, transparent 50%); display: block;"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: flex; align-items: center; justify-content: space-between; gap: 12px; padding-top: 12px; border-top: 1px solid var(--border);">
|
||||
<div style="font-size: 15px;">Язык</div>
|
||||
<div style="display: flex; padding: 3px; border-radius: 11px; border: 1px solid var(--border); flex: none;">
|
||||
<button type="button" onClick="{{ setRu }}" style="height: 34px; padding: 0 16px; border: 0; border-radius: 8px; background: {{ ruBg }}; color: {{ ruInk }}; font-family: inherit; font-size: 14px; cursor: pointer;">RU</button>
|
||||
<button type="button" onClick="{{ setEn }}" style="height: 34px; padding: 0 16px; border: 0; border-radius: 8px; background: {{ enBg }}; color: {{ enInk }}; font-family: inherit; font-size: 14px; cursor: pointer;">EN</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" onClick="{{ signOut }}" style="justify-self: start; min-height: 48px; padding: 0 16px; border-radius: 12px; border: 1px solid var(--border); background: transparent; color: var(--ink-2); font-family: inherit; font-size: 15px; cursor: pointer;">Выйти · anna@example.com</button>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<div hidden="{{ pillOff }}" style="position: fixed; left: 50%; bottom: 18px; transform: translateX(-50%); z-index: 50; display: flex; gap: 4px; padding: 6px; border-radius: 999px; background: color-mix(in oklab, var(--surface) 62%, transparent); backdrop-filter: blur(24px) saturate(1.8); -webkit-backdrop-filter: blur(24px) saturate(1.8); border: 1px solid color-mix(in oklab, var(--ink) 8%, transparent); box-shadow: 0 10px 30px -10px color-mix(in oklab, var(--ink) 40%, transparent);">
|
||||
<button type="button" onClick="{{ goHome }}" style="display: grid; justify-items: center; gap: 4px; min-width: 76px; min-height: 52px; padding: 8px 12px; border: 0; border-radius: 999px; background: {{ tabHomeBg }}; color: {{ tabHomeInk }}; font-family: inherit; font-size: 11px; cursor: pointer;">
|
||||
<span style="position: relative; width: 20px; height: 20px; display: block;">
|
||||
<span style="position: absolute; left: 0; top: 0; width: 9px; height: 9px; border-radius: 2px; background: currentColor; display: block;"></span>
|
||||
<span style="position: absolute; right: 0; top: 0; width: 9px; height: 9px; border-radius: 2px; background: currentColor; opacity: 0.45; display: block;"></span>
|
||||
<span style="position: absolute; left: 0; bottom: 0; width: 9px; height: 9px; border-radius: 2px; background: currentColor; opacity: 0.45; display: block;"></span>
|
||||
<span style="position: absolute; right: 0; bottom: 0; width: 9px; height: 9px; border-radius: 2px; background: currentColor; display: block;"></span>
|
||||
</span>
|
||||
Главная
|
||||
</button>
|
||||
<button type="button" onClick="{{ goKeys }}" style="display: grid; justify-items: center; gap: 4px; min-width: 76px; min-height: 52px; padding: 8px 12px; border: 0; border-radius: 999px; background: {{ tabKeysBg }}; color: {{ tabKeysInk }}; font-family: inherit; font-size: 11px; cursor: pointer;">
|
||||
<span style="position: relative; width: 20px; height: 20px; display: block;">
|
||||
<span style="position: absolute; left: 0; top: 2px; width: 20px; height: 4px; border-radius: 2px; background: currentColor; display: block;"></span>
|
||||
<span style="position: absolute; left: 0; top: 9px; width: 20px; height: 4px; border-radius: 2px; background: currentColor; opacity: 0.6; display: block;"></span>
|
||||
<span style="position: absolute; left: 0; top: 16px; width: 20px; height: 4px; border-radius: 2px; background: currentColor; opacity: 0.3; display: block;"></span>
|
||||
</span>
|
||||
Подписка
|
||||
</button>
|
||||
<button type="button" onClick="{{ goBuy }}" style="display: grid; justify-items: center; gap: 4px; min-width: 76px; min-height: 52px; padding: 8px 12px; border: 0; border-radius: 999px; background: {{ tabBuyBg }}; color: {{ tabBuyInk }}; font-family: inherit; font-size: 11px; cursor: pointer;">
|
||||
<span style="position: relative; width: 20px; height: 20px; display: block;">
|
||||
<span style="position: absolute; left: 8px; top: 0; width: 4px; height: 20px; border-radius: 2px; background: currentColor; display: block;"></span>
|
||||
<span style="position: absolute; left: 0; top: 8px; width: 20px; height: 4px; border-radius: 2px; background: currentColor; display: block;"></span>
|
||||
</span>
|
||||
Купить
|
||||
</button>
|
||||
<button type="button" onClick="{{ goMore }}" style="display: grid; justify-items: center; gap: 4px; min-width: 76px; min-height: 52px; padding: 8px 12px; border: 0; border-radius: 999px; background: {{ tabMoreBg }}; color: {{ tabMoreInk }}; font-family: inherit; font-size: 11px; cursor: pointer;">
|
||||
<span style="position: relative; width: 20px; height: 20px; display: block;">
|
||||
<span style="position: absolute; left: 0; top: 8px; width: 4px; height: 4px; border-radius: 50%; background: currentColor; display: block;"></span>
|
||||
<span style="position: absolute; left: 8px; top: 8px; width: 4px; height: 4px; border-radius: 50%; background: currentColor; display: block;"></span>
|
||||
<span style="position: absolute; left: 16px; top: 8px; width: 4px; height: 4px; border-radius: 50%; background: currentColor; display: block;"></span>
|
||||
</span>
|
||||
Ещё
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div hidden="{{ qrOff }}" onClick="{{ closeQr }}" style="position: fixed; inset: 0; z-index: 60; display: flex; align-items: center; justify-content: center; padding: 24px; background: color-mix(in oklab, var(--ink) 45%, transparent); backdrop-filter: blur(6px);">
|
||||
<div style="display: grid; gap: 16px; justify-items: center; padding: 28px; border-radius: 22px; background: var(--surface); border: 1px solid var(--border); box-shadow: var(--shadow);">
|
||||
<div style="font-size: 16px; font-weight: 500;">{{ qrTitle }}</div>
|
||||
{{ qrImage }}
|
||||
<div style="max-width: 260px; text-align: center; font-size: 13px; line-height: 1.5; color: var(--ink-3);">Откройте приложение на телефоне и наведите камеру на код.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</x-dc>
|
||||
<script type="text/x-dc" data-dc-script data-props="{"defaultTheme":{"editor":"enum","options":["light","dark","system"],"default":"light","tsType":"'light' | 'dark' | 'system'","section":"Тема"}}">
|
||||
class Component extends DCLogic {
|
||||
state = { theme: null, sysDark: false, view: "home", key: 0, faq: 0, loc: 0, lead: false, leadSent: false, qr: null, copied: false, signedIn: true, otp: false, lang: "ru", narrow: false, refCopied: false, topup: false, sum: 1, method: 0, topupSent: false };
|
||||
|
||||
data() {
|
||||
const base = { setup: "4 900 ₽", month: "2 499 ₽" };
|
||||
const rare = { setup: "6 900 ₽", month: "3 499 ₽" };
|
||||
const far = { setup: "9 900 ₽", month: "4 999 ₽" };
|
||||
const L = (country, city, code, lng, lat, ready, tier) => ({ country, city, code, lng, lat, ready, setup: tier.setup, month: tier.month, rare: tier !== base });
|
||||
return [
|
||||
L("Нидерланды", "Амстердам", "nl", 4.9, 52.37, true, base),
|
||||
L("Германия", "Франкфурт", "de", 8.68, 50.11, true, base),
|
||||
L("США", "Нью-Йорк", "us", -74.01, 40.71, true, base),
|
||||
L("Великобритания", "Лондон", "gb", -0.13, 51.51, true, base),
|
||||
L("Франция", "Париж", "fr", 2.35, 48.86, true, base),
|
||||
L("Финляндия", "Хельсинки", "fi", 24.94, 60.17, true, base),
|
||||
L("Швеция", "Стокгольм", "se", 18.07, 59.33, true, base),
|
||||
L("Норвегия", "Осло", "no", 10.75, 59.91, false, base),
|
||||
L("Дания", "Копенгаген", "dk", 12.57, 55.68, false, base),
|
||||
L("Швейцария", "Цюрих", "ch", 8.54, 47.37, true, rare),
|
||||
L("Австрия", "Вена", "at", 16.37, 48.21, true, base),
|
||||
L("Испания", "Мадрид", "es", -3.7, 40.42, true, base),
|
||||
L("Италия", "Милан", "it", 9.19, 45.46, true, base),
|
||||
L("Польша", "Варшава", "pl", 21.01, 52.23, true, base),
|
||||
L("Чехия", "Прага", "cz", 14.44, 50.08, true, base),
|
||||
L("Румыния", "Бухарест", "ro", 26.1, 44.43, false, base),
|
||||
L("Литва", "Вильнюс", "lt", 25.28, 54.69, true, base),
|
||||
L("Латвия", "Рига", "lv", 24.11, 56.95, true, base),
|
||||
L("Эстония", "Таллин", "ee", 24.75, 59.44, false, base),
|
||||
L("Турция", "Стамбул", "tr", 28.98, 41.01, true, base),
|
||||
L("Казахстан", "Алматы", "kz", 76.89, 43.24, false, base),
|
||||
L("Армения", "Ереван", "am", 44.51, 40.18, false, base),
|
||||
L("Грузия", "Тбилиси", "ge", 44.8, 41.72, false, base),
|
||||
L("ОАЭ", "Дубай", "ae", 55.27, 25.2, true, rare),
|
||||
L("Сингапур", "Сингапур", "sg", 103.82, 1.35, true, rare),
|
||||
L("Япония", "Токио", "jp", 139.69, 35.69, true, rare),
|
||||
L("Южная Корея", "Сеул", "kr", 126.98, 37.57, false, rare),
|
||||
L("Канада", "Торонто", "ca", -79.38, 43.65, true, base),
|
||||
L("Австралия", "Сидней", "au", 151.21, -33.87, false, far),
|
||||
L("Бразилия", "Сан-Паулу", "br", -46.63, -23.55, false, far)
|
||||
];
|
||||
}
|
||||
|
||||
flagSpecs() {
|
||||
return {
|
||||
nl: { rows: ["#AE1C28", "#FFFFFF", "#21468B"] },
|
||||
de: { rows: ["#000000", "#DD0000", "#FFCE00"] },
|
||||
us: { rows: ["#B22234", "#FFFFFF", "#B22234", "#FFFFFF", "#B22234", "#FFFFFF"], marks: [{ l: 0, t: 0, w: 22, h: 17, bg: "#3C3B6E" }] },
|
||||
gb: { bg: "#012169", marks: [{ l: 17, t: 0, w: 12, h: 32, bg: "#FFFFFF" }, { l: 0, t: 10, w: 46, h: 12, bg: "#FFFFFF" }, { l: 20, t: 0, w: 6, h: 32, bg: "#C8102E" }, { l: 0, t: 13, w: 46, h: 6, bg: "#C8102E" }] },
|
||||
fr: { cols: ["#002395", "#FFFFFF", "#ED2939"] },
|
||||
fi: { bg: "#FFFFFF", marks: [{ l: 13, t: 0, w: 8, h: 32, bg: "#003580" }, { l: 0, t: 12, w: 46, h: 8, bg: "#003580" }] },
|
||||
se: { bg: "#006AA7", marks: [{ l: 13, t: 0, w: 6, h: 32, bg: "#FECC00" }, { l: 0, t: 13, w: 46, h: 6, bg: "#FECC00" }] },
|
||||
no: { bg: "#BA0C2F", marks: [{ l: 12, t: 0, w: 10, h: 32, bg: "#FFFFFF" }, { l: 0, t: 11, w: 46, h: 10, bg: "#FFFFFF" }, { l: 15, t: 0, w: 4, h: 32, bg: "#00205B" }, { l: 0, t: 14, w: 46, h: 4, bg: "#00205B" }] },
|
||||
dk: { bg: "#C8102E", marks: [{ l: 13, t: 0, w: 6, h: 32, bg: "#FFFFFF" }, { l: 0, t: 13, w: 46, h: 6, bg: "#FFFFFF" }] },
|
||||
ch: { bg: "#D52B1E", marks: [{ l: 20, t: 8, w: 6, h: 16, bg: "#FFFFFF" }, { l: 15, t: 13, w: 16, h: 6, bg: "#FFFFFF" }] },
|
||||
at: { rows: ["#ED2939", "#FFFFFF", "#ED2939"] },
|
||||
es: { rows: ["#AA151B", "#F1BF00", "#AA151B"], sizes: "1fr 2fr 1fr" },
|
||||
it: { cols: ["#008C45", "#F4F5F0", "#CD212A"] },
|
||||
pl: { rows: ["#FFFFFF", "#DC143C"] },
|
||||
cz: { rows: ["#FFFFFF", "#D7141A"], marks: [{ l: 0, t: 0, w: 18, h: 32, bg: "#11457E", clip: "polygon(0 0, 100% 50%, 0 100%)" }] },
|
||||
ro: { cols: ["#002B7F", "#FCD116", "#CE1126"] },
|
||||
lt: { rows: ["#FDB913", "#006A44", "#C1272D"] },
|
||||
lv: { rows: ["#9E3039", "#FFFFFF", "#9E3039"], sizes: "2fr 1fr 2fr" },
|
||||
ee: { rows: ["#0072CE", "#000000", "#FFFFFF"] },
|
||||
tr: { bg: "#E30A17", marks: [{ l: 11, t: 8, w: 16, h: 16, r: "50%", bg: "#FFFFFF" }, { l: 16, t: 8, w: 16, h: 16, r: "50%", bg: "#E30A17" }] },
|
||||
kz: { bg: "#00AFCA", marks: [{ l: 16, t: 8, w: 15, h: 15, r: "50%", bg: "#FEC50C" }] },
|
||||
am: { rows: ["#D90012", "#0033A0", "#F2A800"] },
|
||||
ge: { bg: "#FFFFFF", marks: [{ l: 20, t: 0, w: 6, h: 32, bg: "#FF0000" }, { l: 0, t: 13, w: 46, h: 6, bg: "#FF0000" }] },
|
||||
ae: { rows: ["#00732F", "#FFFFFF", "#000000"], marks: [{ l: 0, t: 0, w: 11, h: 32, bg: "#FF0000" }] },
|
||||
sg: { rows: ["#EF3340", "#FFFFFF"], marks: [{ l: 5, t: 3, w: 11, h: 11, r: "50%", bg: "#FFFFFF" }, { l: 9, t: 3, w: 11, h: 11, r: "50%", bg: "#EF3340" }] },
|
||||
jp: { bg: "#FFFFFF", marks: [{ l: 16, t: 9, w: 14, h: 14, r: "50%", bg: "#BC002D" }] },
|
||||
kr: { bg: "#FFFFFF", marks: [{ l: 16, t: 9, w: 14, h: 14, r: "50%", bg: "#CD2E3A" }, { l: 16, t: 9, w: 14, h: 14, r: "50%", bg: "#0047A0", clip: "polygon(0 50%, 100% 50%, 100% 100%, 0 100%)" }] },
|
||||
ca: { cols: ["#D80621", "#FFFFFF", "#D80621"], sizes: "1fr 2fr 1fr", marks: [{ l: 20, t: 9, w: 7, h: 13, bg: "#D80621", clip: "polygon(50% 0, 100% 100%, 0 100%)" }] },
|
||||
au: { bg: "#00247D", marks: [{ l: 9, t: 0, w: 4, h: 16, bg: "#FFFFFF" }, { l: 0, t: 6, w: 22, h: 4, bg: "#FFFFFF" }, { l: 30, t: 19, w: 7, h: 7, r: "50%", bg: "#FFFFFF" }] },
|
||||
br: { bg: "#009739", marks: [{ l: 9, t: 4, w: 28, h: 24, bg: "#FEDD00", clip: "polygon(50% 0, 100% 50%, 50% 100%, 0 50%)" }, { l: 18, t: 11, w: 10, h: 10, r: "50%", bg: "#012169" }] }
|
||||
};
|
||||
}
|
||||
|
||||
flag(code) {
|
||||
const h = React.createElement;
|
||||
const spec = this.flagSpecs()[code] || {};
|
||||
const style = {
|
||||
width: "46px", height: "32px", borderRadius: "6px", overflow: "hidden",
|
||||
display: "grid", border: "1px solid var(--border)", position: "relative", flex: "none"
|
||||
};
|
||||
let bands = [];
|
||||
if (spec.rows) {
|
||||
bands = spec.rows;
|
||||
style.gridTemplateRows = spec.sizes || "repeat(" + bands.length + ", 1fr)";
|
||||
} else if (spec.cols) {
|
||||
bands = spec.cols;
|
||||
style.gridAutoFlow = "column";
|
||||
style.gridTemplateColumns = spec.sizes || "repeat(" + bands.length + ", 1fr)";
|
||||
} else {
|
||||
style.background = spec.bg || "var(--surface-2)";
|
||||
}
|
||||
const kids = bands.map((c, i) => h("span", { key: "b" + i, style: { background: c, display: "block" } }));
|
||||
(spec.marks || []).forEach((m, i) => kids.push(h("span", {
|
||||
key: "m" + i,
|
||||
style: {
|
||||
position: "absolute", left: m.l + "px", top: m.t + "px", width: m.w + "px", height: m.h + "px",
|
||||
borderRadius: m.r || 0, background: m.bg, clipPath: m.clip || "none", display: "block"
|
||||
}
|
||||
})));
|
||||
return h("span", { style: style }, kids);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const mq = window.matchMedia("(prefers-color-scheme: dark)");
|
||||
this.mq = mq;
|
||||
this.onSys = () => this.setState({ sysDark: mq.matches });
|
||||
mq.addEventListener("change", this.onSys);
|
||||
this.onResize = () => {
|
||||
const n = window.innerWidth < 900;
|
||||
if (n !== this.state.narrow) this.setState({ narrow: n, view: this.state.view === "more" && !n ? "home" : this.state.view });
|
||||
if (!n && this.state.view === "more") this.setState({ view: "home" });
|
||||
};
|
||||
window.addEventListener("resize", this.onResize);
|
||||
this.onResize();
|
||||
let saved = null;
|
||||
try { saved = localStorage.getItem("veilix.theme"); } catch (e) {}
|
||||
let savedLang = null;
|
||||
try { savedLang = localStorage.getItem("veilix.lang"); } catch (e) {}
|
||||
this.setState({ sysDark: mq.matches, theme: saved || this.props.defaultTheme || "light", lang: savedLang || "ru" }, () => { this.apply(); this.applyLang(); });
|
||||
}
|
||||
|
||||
applyLang() {
|
||||
const lang = this.state.lang || "ru";
|
||||
if (window.VEILIX_APPLY_LANG) window.VEILIX_APPLY_LANG(document.body, lang);
|
||||
else setTimeout(() => this.applyLang(), 150);
|
||||
}
|
||||
|
||||
setLang(l) {
|
||||
try { localStorage.setItem("veilix.lang", l); } catch (e) {}
|
||||
this.setState({ lang: l }, () => this.applyLang());
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.mq) this.mq.removeEventListener("change", this.onSys);
|
||||
if (this.onResize) window.removeEventListener("resize", this.onResize);
|
||||
}
|
||||
componentDidUpdate() { this.apply(); this.applyLang(); }
|
||||
|
||||
apply() {
|
||||
const theme = this.state.theme || this.props.defaultTheme || "light";
|
||||
const dark = theme === "dark" || (theme === "system" && this.state.sysDark);
|
||||
document.documentElement.setAttribute("data-veilix-theme", dark ? "dark" : "light");
|
||||
}
|
||||
|
||||
setTheme(t) {
|
||||
try { localStorage.setItem("veilix.theme", t); } catch (e) {}
|
||||
this.setState({ theme: t }, () => this.apply());
|
||||
}
|
||||
|
||||
qrImage(seed) {
|
||||
const n = 21;
|
||||
let h = 0;
|
||||
for (let i = 0; i < seed.length; i++) h = (h * 31 + seed.charCodeAt(i)) >>> 0;
|
||||
const cells = [];
|
||||
const finder = (r, c) => (r < 7 && c < 7) || (r < 7 && c >= n - 7) || (r >= n - 7 && c < 7);
|
||||
for (let r = 0; r < n; r++) {
|
||||
for (let c = 0; c < n; c++) {
|
||||
let on;
|
||||
if (finder(r, c)) {
|
||||
const rr = r < 7 ? r : n - 1 - r;
|
||||
const cc = c < 7 ? c : n - 1 - c;
|
||||
const d = Math.max(Math.abs(rr - 3), Math.abs(cc - 3));
|
||||
on = d !== 2;
|
||||
} else {
|
||||
h = (h * 1103515245 + 12345) >>> 0;
|
||||
on = ((h >>> 16) & 1) === 1;
|
||||
}
|
||||
cells.push(React.createElement("span", {
|
||||
key: r + "-" + c,
|
||||
style: { background: on ? "var(--ink)" : "transparent", display: "block" }
|
||||
}));
|
||||
}
|
||||
}
|
||||
return React.createElement("div", {
|
||||
style: {
|
||||
display: "grid",
|
||||
gridTemplateColumns: "repeat(" + n + ", 1fr)",
|
||||
width: "220px",
|
||||
height: "220px",
|
||||
padding: "12px",
|
||||
borderRadius: "14px",
|
||||
background: "var(--bg)",
|
||||
border: "1px solid var(--border)"
|
||||
}
|
||||
}, cells);
|
||||
}
|
||||
|
||||
renderVals() {
|
||||
const s = this.state;
|
||||
const sel = "var(--sel)";
|
||||
const theme = s.theme || this.props.defaultTheme || "light";
|
||||
const keyData = [
|
||||
{ title: "Нидерланды · Амстердам", ip: "текущая локация", until: s.lang === "en" ? "14 October 2026" : "14 октября 2026", badge: "Активна", warn: false, devices: "3 из 5 устройств", payment: "Карта ···· 4242", card: "···· 4242", left: "Продлится автоматически через 31 день", xray: "vless://8f2a…@nl1.veilixvpn.net:443?type=xhttp#VeilixVPN-NL" },
|
||||
{ title: "США · Нью-Йорк", ip: "запасная локация", until: s.lang === "en" ? "14 October 2026" : "14 октября 2026", badge: "Готова", warn: false, devices: "1 из 5 устройств", payment: "Карта ···· 4242", card: "···· 4242", left: "Переключиться можно в любой момент", xray: "vless://c41d…@us1.veilixvpn.net:443?type=xhttp#VeilixVPN-US" }
|
||||
];
|
||||
const cur = keyData[s.key];
|
||||
const mk = (i) => ({
|
||||
title: keyData[i].title,
|
||||
ip: keyData[i].ip,
|
||||
until: keyData[i].until,
|
||||
untilLine: (s.lang === "en" ? "until " : "до ") + keyData[i].until,
|
||||
badge: keyData[i].badge,
|
||||
devices: keyData[i].devices,
|
||||
payment: keyData[i].payment,
|
||||
badgeBg: keyData[i].warn ? "color-mix(in oklab, var(--warn) 18%, transparent)" : sel,
|
||||
badgeInk: keyData[i].warn ? "var(--warn)" : "var(--accent)",
|
||||
open: () => this.setState({ view: "key", key: i })
|
||||
});
|
||||
const faqData = [
|
||||
["Как подключить новое устройство?", "Откройте подключение, скачайте любую свободную конфигурацию XRAY или отсканируйте её QR-код. Каждая конфигурация рассчитана на одно устройство."],
|
||||
["Что делать, если подключение перестало работать?", "Обновите конфигурацию в кабинете или смените локацию — это занимает пару секунд. Если не помогло, напишите в поддержку, ответим в течение часа."],
|
||||
["Как отменить продление?", "В разделе оплаты нажмите «Изменить карту» и отвяжите её. Подписка доработает оплаченный период."]
|
||||
];
|
||||
const locData = [
|
||||
["Нидерланды", "Амстердам", true],
|
||||
["Германия", "Франкфурт", true],
|
||||
["США", "Нью-Йорк", true],
|
||||
["Финляндия", "Хельсинки", false],
|
||||
["Турция", "Стамбул", false],
|
||||
["Япония", "Токио", false]
|
||||
];
|
||||
const cfgNames = [
|
||||
["Ноутбук", "подключено"],
|
||||
["Телефон", "подключено"],
|
||||
["Планшет", "подключено"],
|
||||
["Рабочий ноутбук", "не занят"],
|
||||
["Свободный слот", "не занят"]
|
||||
];
|
||||
|
||||
return {
|
||||
appOff: s.signedIn,
|
||||
loginOff: !s.signedIn,
|
||||
otpSent: s.otp,
|
||||
otpNotSent: !s.otp,
|
||||
sendOtp: () => this.setState({ otp: true }),
|
||||
signIn: () => this.setState({ signedIn: true, otp: false, view: "home" }),
|
||||
backToEmail: () => this.setState({ otp: false }),
|
||||
signOut: () => this.setState({ signedIn: false, otp: false }),
|
||||
|
||||
goHome: () => this.setState({ view: "home" }),
|
||||
goKeys: () => this.setState({ view: "keys" }),
|
||||
goBuy: () => this.setState({ view: "buy" }),
|
||||
goMore: () => this.setState({ view: "more" }),
|
||||
goBilling: () => this.setState({ view: "billing" }),
|
||||
billingOff: s.view !== "billing",
|
||||
billingBg: s.view === "billing" ? sel : "transparent",
|
||||
billingInk: s.view === "billing" ? "var(--accent)" : "var(--ink-2)",
|
||||
openTopup: () => this.setState({ topup: true }),
|
||||
closeTopup: () => this.setState({ topup: false }),
|
||||
topupOff: !s.topup,
|
||||
sums: ["700 ₽", "2 100 ₽", "6 990 ₽", "Своя сумма"].map((label, i) => ({
|
||||
label: label,
|
||||
bg: s.sum === i ? sel : "transparent",
|
||||
border: s.sum === i ? "var(--accent)" : "var(--border)",
|
||||
pick: () => this.setState({ sum: i })
|
||||
})),
|
||||
methods: ["Карта ···· 4242", "СБП", "Криптовалюта"].map((label, i) => ({
|
||||
label: label,
|
||||
bg: s.method === i ? sel : "transparent",
|
||||
border: s.method === i ? "var(--accent)" : "var(--border)",
|
||||
pick: () => this.setState({ method: i })
|
||||
})),
|
||||
cryptoOff: s.method !== 2,
|
||||
sendTopup: () => this.setState({ topupSent: true }),
|
||||
topupButton: s.topupSent ? "Счёт выставлен" : "Пополнить",
|
||||
payments: [
|
||||
{ date: "14 сентября", title: "Подписка на месяц", method: "Карта ···· 4242", status: "Оплачено", amount: "−699 ₽", ink: "var(--ink)" },
|
||||
{ date: "6 сентября", title: "Пополнение баланса", method: "USDT (TRC-20)", status: "Зачислено", amount: "+2 100 ₽", ink: "var(--accent)" },
|
||||
{ date: "14 августа", title: "Подписка на месяц", method: "С баланса", status: "Оплачено", amount: "−699 ₽", ink: "var(--ink)" },
|
||||
{ date: "14 июля", title: "Подписка на месяц", method: "Карта ···· 4242", status: "Оплачено", amount: "−699 ₽", ink: "var(--ink)" },
|
||||
{ date: "2 июля", title: "Реферальный бонус", method: "Месяц в подарок", status: "Начислено", amount: "+699 ₽", ink: "var(--accent)" }
|
||||
],
|
||||
goRef: () => this.setState({ view: "ref" }),
|
||||
refOff: s.view !== "ref",
|
||||
refBg: s.view === "ref" ? sel : "transparent",
|
||||
refInk: s.view === "ref" ? "var(--accent)" : "var(--ink-2)",
|
||||
setRu: () => this.setLang("ru"),
|
||||
setEn: () => this.setLang("en"),
|
||||
ruBg: s.lang === "en" ? "transparent" : sel,
|
||||
enBg: s.lang === "en" ? sel : "transparent",
|
||||
ruInk: s.lang === "en" ? "var(--ink-3)" : "var(--ink)",
|
||||
enInk: s.lang === "en" ? "var(--ink)" : "var(--ink-3)",
|
||||
homeOff: s.view !== "home",
|
||||
keysOff: s.view !== "keys",
|
||||
keyOff: s.view !== "key",
|
||||
buyOff: s.view !== "buy",
|
||||
moreOff: s.view !== "more",
|
||||
shellCols: s.narrow ? "minmax(0, 1fr)" : "248px minmax(0, 1fr)",
|
||||
mainPad: s.narrow ? "20px 16px 110px" : "36px 40px 64px",
|
||||
asideOff: s.narrow,
|
||||
topbarOff: !s.narrow,
|
||||
pillOff: !s.narrow,
|
||||
tabHomeBg: s.view === "home" ? sel : "transparent",
|
||||
tabKeysBg: (s.view === "keys" || s.view === "key") ? sel : "transparent",
|
||||
tabBuyBg: s.view === "buy" ? sel : "transparent",
|
||||
tabMoreBg: s.view === "more" ? sel : "transparent",
|
||||
tabHomeInk: s.view === "home" ? "var(--accent)" : "var(--ink-2)",
|
||||
tabKeysInk: (s.view === "keys" || s.view === "key") ? "var(--accent)" : "var(--ink-2)",
|
||||
tabBuyInk: s.view === "buy" ? "var(--accent)" : "var(--ink-2)",
|
||||
tabMoreInk: s.view === "more" ? "var(--accent)" : "var(--ink-2)",
|
||||
homeBg: s.view === "home" ? sel : "transparent",
|
||||
keysBg: (s.view === "keys" || s.view === "key") ? sel : "transparent",
|
||||
buyBg: s.view === "buy" ? sel : "transparent",
|
||||
homeInk: s.view === "home" ? "var(--accent)" : "var(--ink-2)",
|
||||
keysInk: (s.view === "keys" || s.view === "key") ? "var(--accent)" : "var(--ink-2)",
|
||||
buyInk: s.view === "buy" ? "var(--accent)" : "var(--ink-2)",
|
||||
|
||||
keys: [mk(0), mk(1)],
|
||||
allKeys: [mk(0), mk(1)],
|
||||
cur: {
|
||||
title: cur.title,
|
||||
ip: cur.ip,
|
||||
until: cur.until,
|
||||
card: cur.card,
|
||||
devices: cur.devices,
|
||||
left: cur.left,
|
||||
leftInk: cur.warn ? "var(--warn)" : "var(--ink-2)",
|
||||
xray: cur.xray
|
||||
},
|
||||
configs: cfgNames.map((c, i) => ({
|
||||
name: c[0],
|
||||
state: c[1],
|
||||
showQr: () => this.setState({ qr: { title: "XRAY · " + c[0], seed: cur.ip + c[0] } })
|
||||
})),
|
||||
refStats: s.lang === "en" ? "Invited: 3 · Months earned: 2" : "Приглашено: 3 · Месяцев начислено: 2",
|
||||
copyRef: () => this.setState({ refCopied: true }),
|
||||
refLabel: s.refCopied ? "Ссылка скопирована" : "Копировать ссылку",
|
||||
copyXray: () => this.setState({ copied: true }),
|
||||
copyLabel: s.copied ? "Скопировано" : "Копировать",
|
||||
showXrayQr: () => this.setState({ qr: { title: "XRAY · Happ", seed: cur.xray } }),
|
||||
qrOff: !s.qr,
|
||||
qrTitle: s.qr ? s.qr.title : "",
|
||||
qrImage: s.qr ? this.qrImage(s.qr.seed) : null,
|
||||
closeQr: () => this.setState({ qr: null }),
|
||||
|
||||
faq: faqData.map((row, i) => ({
|
||||
q: row[0],
|
||||
a: row[1],
|
||||
off: s.faq !== i,
|
||||
rot: s.faq === i ? "rotate(-135deg) translateY(2px)" : "rotate(45deg) translateY(-2px)",
|
||||
toggle: () => this.setState(st => ({ faq: st.faq === i ? -1 : i }))
|
||||
})),
|
||||
|
||||
locations: this.data().map((d, i) => ({
|
||||
country: d.country,
|
||||
city: d.city,
|
||||
flag: this.flag(d.code),
|
||||
status: d.ready ? "Доступно в подписке" : "Скоро",
|
||||
statusInk: d.ready ? "var(--accent)" : "var(--ink-3)",
|
||||
bg: s.loc === i ? sel : "var(--surface)",
|
||||
border: s.loc === i ? "var(--accent)" : "var(--border)",
|
||||
dot: s.loc === i ? "var(--accent)" : "transparent",
|
||||
dotOff: s.loc === i,
|
||||
setup: d.setup,
|
||||
month: d.month,
|
||||
buyOff: s.loc !== i,
|
||||
pick: () => this.setState({ loc: i })
|
||||
})),
|
||||
openLead: () => this.setState({ lead: true }),
|
||||
sendLead: () => this.setState({ leadSent: true }),
|
||||
leadOpen: s.lead,
|
||||
leadClosed: !s.lead,
|
||||
leadButton: s.leadSent ? "Заявка отправлена" : "Отправить",
|
||||
pickedTitle: locData[s.loc][0] + " · " + locData[s.loc][1],
|
||||
pickedHint: "Локация доступна сразу, переключение бесплатное",
|
||||
|
||||
toggleLang: () => this.setLang(s.lang === "ru" ? "en" : "ru"),
|
||||
langTitle: s.lang === "ru" ? "Язык: русский" : "Язык: английский",
|
||||
langLabel: s.lang === "ru" ? "RU" : "EN",
|
||||
setLight: () => this.setTheme("light"),
|
||||
setDark: () => this.setTheme("dark"),
|
||||
setSystem: () => this.setTheme("system"),
|
||||
lightBg: theme === "light" ? sel : "transparent",
|
||||
darkBg: theme === "dark" ? sel : "transparent",
|
||||
systemBg: theme === "system" ? sel : "transparent",
|
||||
lightInk: theme === "light" ? "var(--ink)" : "var(--ink-3)",
|
||||
darkInk: theme === "dark" ? "var(--ink)" : "var(--ink-3)",
|
||||
systemInk: theme === "system" ? "var(--ink)" : "var(--ink-3)"
|
||||
};
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,806 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<script src="./support.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<x-dc>
|
||||
<helmet>
|
||||
<link rel="icon" type="image/svg+xml" href="favicon-mass.svg" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin="anonymous" />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Commissioner:wght@300;400;500;600&family=Unbounded:wght@500;600&display=swap" rel="stylesheet" />
|
||||
<script>(function(){if(window.__veilixI18nSrc)return;window.__veilixI18nSrc="./i18n.js?v="+Date.now().toString(36)+Math.random().toString(36).slice(2,8);var s=document.createElement("script");s.src=window.__veilixI18nSrc;document.head.appendChild(s);})();</script>
|
||||
<style>
|
||||
:root{
|
||||
--bg: oklch(99.6% 0.0015 215);
|
||||
--surface: oklch(100% 0 0);
|
||||
--surface-2: oklch(98.4% 0.003 215);
|
||||
--ink: oklch(24% 0.04 204);
|
||||
--ink-2: oklch(45% 0.03 204);
|
||||
--ink-3: oklch(60% 0.025 204);
|
||||
--border: oklch(92% 0.014 200);
|
||||
--accent: oklch(48% 0.105 178);
|
||||
--accent-soft: oklch(95.5% 0.032 180);
|
||||
--sel: oklch(96.5% 0.016 190);
|
||||
--on-accent: oklch(99% 0.01 180);
|
||||
--glow-3: oklch(89% 0.07 172);
|
||||
--veil: oklch(100% 0 0 / 0.55);
|
||||
--warn: oklch(62% 0.14 62);
|
||||
--tg: oklch(62% 0.15 250);
|
||||
}
|
||||
:root[data-veilix-theme="dark"]{
|
||||
--bg: oklch(23% 0.026 210);
|
||||
--surface: oklch(26.5% 0.028 210);
|
||||
--surface-2: oklch(30% 0.03 210);
|
||||
--ink: oklch(96% 0.012 196);
|
||||
--ink-2: oklch(80% 0.02 196);
|
||||
--ink-3: oklch(65% 0.025 196);
|
||||
--border: oklch(36% 0.04 204);
|
||||
--accent: oklch(76% 0.105 190);
|
||||
--accent-soft: oklch(31% 0.05 190);
|
||||
--sel: oklch(30% 0.03 195);
|
||||
--on-accent: oklch(20% 0.04 204);
|
||||
--glow-3: oklch(50% 0.08 180);
|
||||
--veil: oklch(100% 0 0 / 0.07);
|
||||
--warn: oklch(78% 0.13 62);
|
||||
--tg: oklch(70% 0.13 250);
|
||||
}
|
||||
*{ box-sizing: border-box; }
|
||||
input, textarea, select{ font-size: 16px; }
|
||||
[hidden]{ display: none !important; }
|
||||
body{ margin:0; background: oklch(95% 0.008 296); color: var(--ink); font-family:"Commissioner", system-ui, sans-serif; -webkit-font-smoothing: antialiased; }
|
||||
:root[data-veilix-theme="dark"] body{ background: oklch(13% 0.018 296); }
|
||||
a{ color: var(--accent); text-decoration: none; }
|
||||
input::placeholder, textarea::placeholder{ color: var(--ink-3); }
|
||||
</style>
|
||||
</helmet>
|
||||
|
||||
<template id="__bundler_thumbnail" data-bg-color="#e3f4f1">
|
||||
<svg viewBox="0 0 1200 800" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="480" y="290" width="150" height="150" rx="48" fill="#00705e" />
|
||||
<rect x="570" y="360" width="150" height="150" rx="48" fill="#ffffff" fill-opacity="0.62" stroke="#00705e" stroke-width="6" />
|
||||
</svg>
|
||||
</template>
|
||||
<div style="min-height: 100vh; display: flex; flex-wrap: wrap; align-items: flex-start; justify-content: center; gap: 32px; padding: 40px 24px 64px;">
|
||||
|
||||
<x-import component-from-global-scope="IOSDevice" from="./ios-frame.jsx?v=20260915" hint-size="393px,852px">
|
||||
<div style="display: flex; flex-direction: column; height: 100%; padding-top: 62px; padding-bottom: 34px; background: var(--bg); color: var(--ink); font-family: 'Commissioner', system-ui, sans-serif; letter-spacing: -0.01em;">
|
||||
|
||||
<div style="display: flex; align-items: center; gap: 12px; padding: 10px 16px; border-bottom: 1px solid var(--border); background: var(--surface);">
|
||||
<span style="position: relative; width: 24px; height: 24px; display: block; flex: none;">
|
||||
<span style="position: absolute; left: 0; top: 3px; width: 18px; height: 18px; border-radius: 6px; background: linear-gradient(140deg, var(--accent), color-mix(in oklab, var(--accent) 55%, var(--glow-3))); display: block;"></span>
|
||||
<span style="position: absolute; right: 0; bottom: 3px; width: 18px; height: 18px; border-radius: 6px; background: var(--veil); border: 1px solid color-mix(in oklab, var(--accent) 35%, transparent); display: block;"></span>
|
||||
</span>
|
||||
<span style="display: grid; gap: 1px; flex: 1; min-width: 0;">
|
||||
<span style="font-size: 15px; font-weight: 500; letter-spacing: -0.02em;">ВейликсВПН</span>
|
||||
<span style="font-size: 11px; color: var(--tg);">мини-приложение</span>
|
||||
</span>
|
||||
<span style="font-size: 15px; color: var(--tg);">Закрыть</span>
|
||||
</div>
|
||||
|
||||
<div style="flex: 1; min-height: 0; overflow: auto; padding: 16px 16px 108px;">
|
||||
|
||||
<div hidden="{{ buyOff }}" style="display: grid; gap: 12px;">
|
||||
<div style="display: grid; gap: 4px; padding: 2px;">
|
||||
<div style="font-size: 22px; font-weight: 600; letter-spacing: -0.03em;">VPN, который не нужно отключать</div>
|
||||
</div>
|
||||
|
||||
<div style="display: grid; gap: 9px; padding: 16px; border-radius: 18px; background: var(--surface); border: 1px solid var(--border);">
|
||||
<div style="display: flex; gap: 10px; font-size: 14px; line-height: 1.45;">
|
||||
<span style="width: 6px; height: 6px; margin-top: 7px; border-radius: 50%; background: var(--accent); display: block; flex: none;"></span>
|
||||
<span>Все 30 локаций в подписке</span>
|
||||
</div>
|
||||
<div style="display: flex; gap: 10px; font-size: 14px; line-height: 1.45;">
|
||||
<span style="width: 6px; height: 6px; margin-top: 7px; border-radius: 50%; background: var(--accent); display: block; flex: none;"></span>
|
||||
<span>Российский адрес в комплекте</span>
|
||||
</div>
|
||||
<div style="display: flex; gap: 10px; font-size: 14px; line-height: 1.45;">
|
||||
<span style="width: 6px; height: 6px; margin-top: 7px; border-radius: 50%; background: var(--accent); display: block; flex: none;"></span>
|
||||
<span>До 5 устройств</span>
|
||||
</div>
|
||||
<div style="display: flex; gap: 10px; font-size: 14px; line-height: 1.45;">
|
||||
<span style="width: 6px; height: 6px; margin-top: 7px; border-radius: 50%; background: var(--accent); display: block; flex: none;"></span>
|
||||
<span>Современный протокол XRAY (VLESS, Hysteria2, XHTTP)</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; gap: 8px;">
|
||||
<sc-for list="{{ plans }}" as="plan" hint-placeholder-count="2">
|
||||
<button type="button" onClick="{{ plan.pick }}" style="flex: 1; display: grid; align-content: start; gap: 4px; min-height: 44px; padding: 14px; border-radius: 16px; border: 1px solid {{ plan.border }}; background: {{ plan.bg }}; color: var(--ink); font-family: inherit; text-align: left; cursor: pointer;">
|
||||
<span style="font-size: 13px; color: var(--ink-3);">{{ plan.name }}</span>
|
||||
<span style="font-size: 20px; font-weight: 600; letter-spacing: -0.03em; font-variant-numeric: tabular-nums;">{{ plan.price }}</span>
|
||||
<span style="font-size: 12px; color: {{ plan.noteInk }};">{{ plan.note }}</span>
|
||||
</button>
|
||||
</sc-for>
|
||||
</div>
|
||||
<button type="button" onClick="{{ confirmPay }}" style="min-height: 52px; border-radius: 16px; border: 0; background: var(--accent); color: var(--on-accent); font-family: inherit; font-size: 16px; font-weight: 500; cursor: pointer;">{{ payButton }}</button>
|
||||
|
||||
<div style="display: flex; align-items: baseline; justify-content: space-between; gap: 10px; padding: 12px 2px 0;">
|
||||
<span style="font-size: 16px; font-weight: 500;">Локации</span>
|
||||
<span style="font-size: 12px; color: var(--ink-3);">{{ locCount }}</span>
|
||||
</div>
|
||||
|
||||
<sc-for list="{{ locations }}" as="loc" hint-placeholder-count="8">
|
||||
<div style="display: flex; align-items: center; gap: 11px; padding: 12px 14px; border-radius: 16px; background: var(--surface); border: 1px solid var(--border);">
|
||||
{{ loc.flag }}
|
||||
<span style="flex: 1; min-width: 0; display: grid; gap: 2px;">
|
||||
<span style="font-size: 15px; font-weight: 500;">{{ loc.country }}</span>
|
||||
<span style="font-size: 12px; color: var(--ink-3);">{{ loc.city }}</span>
|
||||
</span>
|
||||
<span style="flex: none; font-size: 12px; color: {{ loc.statusInk }};">{{ loc.status }}</span>
|
||||
</div>
|
||||
</sc-for>
|
||||
|
||||
<a href="Main.dc.html" style="display: grid; gap: 6px; margin-top: 8px; padding: 16px; border-radius: 18px; background: var(--surface-2); border: 1px solid var(--border); color: var(--ink);">
|
||||
<span style="font-size: 16px; font-weight: 500;">Нужен свой IP-адрес</span>
|
||||
<span style="font-size: 13px; line-height: 1.5; color: var(--ink-2);">Если сервисы требуют постоянный адрес, есть отдельный тариф с индивидуальным IP в выбранной стране.</span>
|
||||
<span style="font-size: 13px; color: var(--accent);">Посмотреть индивидуальный тариф →</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div hidden="{{ faqOff }}" style="display: grid; gap: 10px;">
|
||||
<div style="display: grid; gap: 4px; padding: 2px 2px 6px;">
|
||||
<div style="font-size: 22px; font-weight: 600; letter-spacing: -0.03em;">Частые вопросы</div>
|
||||
<div style="font-size: 13px; color: var(--ink-2);">Если ответа нет — напишите в поддержку.</div>
|
||||
</div>
|
||||
<sc-for list="{{ faq }}" as="item" hint-placeholder-count="6">
|
||||
<div style="border: 1px solid var(--border); border-radius: 16px; background: var(--surface); overflow: hidden;">
|
||||
<button type="button" onClick="{{ item.toggle }}" style="display: flex; align-items: center; gap: 12px; width: 100%; min-height: 52px; padding: 14px 16px; background: transparent; border: 0; color: var(--ink); font-family: inherit; font-size: 15px; font-weight: 500; text-align: left; cursor: pointer;">
|
||||
<span style="flex: 1;">{{ item.q }}</span>
|
||||
<span style="flex: none; width: 8px; height: 8px; border-right: 1.5px solid var(--ink-3); border-bottom: 1.5px solid var(--ink-3); transform: {{ item.rot }}; transition: transform 180ms ease; display: block;"></span>
|
||||
</button>
|
||||
<div hidden="{{ item.off }}" style="padding: 0 16px 16px; font-size: 14px; line-height: 1.55; color: var(--ink-2);">{{ item.a }}</div>
|
||||
</div>
|
||||
</sc-for>
|
||||
<div style="display: flex; align-items: center; gap: 12px; min-height: 52px; padding: 12px 16px; border-radius: 16px; border: 1px solid var(--border); background: var(--surface); font-size: 15px;">
|
||||
<span style="flex: 1;">Остался вопрос</span>
|
||||
<span style="color: var(--tg);">@veilix_support</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div hidden="{{ keysOff }}" style="display: grid; gap: 12px;">
|
||||
<div style="display: grid; gap: 4px; padding: 2px 2px 4px;">
|
||||
<div style="font-size: 22px; font-weight: 600; letter-spacing: -0.03em;">Ваши подписки</div>
|
||||
<div style="font-size: 13px; color: var(--ink-2);">Нажмите, чтобы открыть подключение</div>
|
||||
</div>
|
||||
<sc-for list="{{ keys }}" as="key" hint-placeholder-count="2">
|
||||
<button type="button" onClick="{{ key.open }}" style="display: grid; gap: 10px; width: 100%; min-height: 44px; padding: 16px; border-radius: 18px; border: 1px solid var(--border); background: var(--surface); color: var(--ink); font-family: inherit; text-align: left; cursor: pointer;">
|
||||
<span style="display: flex; align-items: center; gap: 10px;">
|
||||
{{ key.flag }}
|
||||
<span style="flex: 1; min-width: 0; font-size: 16px; font-weight: 500;">{{ key.title }}</span>
|
||||
<span style="padding: 3px 9px; border-radius: 7px; background: {{ key.badgeBg }}; color: {{ key.badgeInk }}; font-size: 11px;">{{ key.badge }}</span>
|
||||
</span>
|
||||
<span style="display: flex; flex-wrap: wrap; gap: 4px 14px; font-size: 13px; color: var(--ink-2); font-variant-numeric: tabular-nums;">
|
||||
<span>{{ key.devices }}</span>
|
||||
<span>{{ key.untilLine }}</span>
|
||||
</span>
|
||||
</button>
|
||||
</sc-for>
|
||||
<button type="button" onClick="{{ goBuy }}" style="display: flex; align-items: center; justify-content: center; gap: 8px; width: 100%; min-height: 50px; border-radius: 16px; border: 1px dashed var(--border); background: var(--surface-2); color: var(--ink-2); font-family: inherit; font-size: 15px; cursor: pointer;">Купить ещё одну подписку</button>
|
||||
<div style="font-size: 12px; line-height: 1.5; color: var(--ink-3); padding: 0 4px;">Каждая подписка — свои пять устройств и своя страна. Оплата общая, со счёта аккаунта.</div>
|
||||
</div>
|
||||
|
||||
<div hidden="{{ keyOff }}" style="display: grid; gap: 14px;">
|
||||
<button type="button" onClick="{{ goKeys }}" style="justify-self: start; min-height: 44px; border: 0; background: transparent; color: var(--tg); font-family: inherit; font-size: 15px; cursor: pointer; padding: 0;">← Подписки</button>
|
||||
<div style="display: grid; gap: 4px;">
|
||||
<div style="font-size: 22px; font-weight: 600; letter-spacing: -0.03em;">{{ curTitle }}</div>
|
||||
<div style="font-size: 13px; color: var(--ink-3);">{{ curPlan }}</div>
|
||||
</div>
|
||||
|
||||
<div style="display: grid; gap: 10px; padding: 16px; border-radius: 18px; background: var(--surface); border: 1px solid var(--border);">
|
||||
<div style="display: flex; align-items: center; gap: 11px;">
|
||||
{{ curFlag }}
|
||||
<span style="flex: 1; min-width: 0; display: grid; gap: 2px;">
|
||||
<span style="font-size: 16px; font-weight: 500;">{{ curCountry }}</span>
|
||||
<span style="font-size: 12px; color: var(--accent);">Подключено сейчас</span>
|
||||
</span>
|
||||
</div>
|
||||
<button type="button" onClick="{{ goSwitch }}" style="min-height: 46px; border-radius: 12px; border: 1px solid var(--border); background: transparent; color: var(--ink); font-family: inherit; font-size: 15px; cursor: pointer;">Сменить страну</button>
|
||||
</div>
|
||||
|
||||
<div style="display: grid; gap: 10px; padding: 16px; border-radius: 18px; background: var(--surface); border: 1px solid var(--border);">
|
||||
<div style="display: flex; align-items: baseline; justify-content: space-between; gap: 10px;">
|
||||
<span style="font-size: 13px; color: var(--ink-3);">Действует до</span>
|
||||
<span style="font-size: 16px; font-weight: 500;">{{ curUntil }}</span>
|
||||
</div>
|
||||
<div style="display: flex; align-items: baseline; justify-content: space-between; gap: 10px;">
|
||||
<span style="font-size: 13px; color: var(--ink-3);">Списание</span>
|
||||
<span style="font-size: 15px; font-variant-numeric: tabular-nums;">{{ curPlan }} · со счёта</span>
|
||||
</div>
|
||||
<div style="font-size: 13px; color: {{ curLeftInk }};">{{ curLeft }}</div>
|
||||
<button type="button" onClick="{{ goBilling }}" style="min-height: 44px; border-radius: 12px; border: 1px solid var(--border); background: transparent; color: var(--ink); font-family: inherit; font-size: 15px; cursor: pointer;">Счёт и платежи</button>
|
||||
</div>
|
||||
|
||||
<div style="display: grid; gap: 10px; padding: 16px; border-radius: 18px; background: var(--surface); border: 1px solid var(--border);">
|
||||
<div style="display: flex; align-items: baseline; justify-content: space-between; gap: 10px;">
|
||||
<span style="font-size: 16px; font-weight: 500;">Устройства</span>
|
||||
<span style="font-size: 12px; color: var(--ink-3);">{{ curDevices }}</span>
|
||||
</div>
|
||||
<sc-for list="{{ configs }}" as="cfg" hint-placeholder-count="5">
|
||||
<div style="display: flex; align-items: center; gap: 10px; padding: 10px 12px; border-radius: 14px; background: var(--surface-2);">
|
||||
<span style="flex: 1; min-width: 0; display: grid; gap: 2px;">
|
||||
<span style="font-size: 15px;">{{ cfg.name }}</span>
|
||||
<span style="font-size: 12px; color: var(--ink-3);">{{ cfg.state }}</span>
|
||||
</span>
|
||||
<button type="button" onClick="{{ cfg.showQr }}" style="min-height: 44px; padding: 0 14px; border-radius: 11px; border: 0; background: var(--accent); color: var(--on-accent); font-family: inherit; font-size: 14px; cursor: pointer;">QR</button>
|
||||
<button type="button" style="min-height: 44px; padding: 0 14px; border-radius: 11px; border: 1px solid var(--border); background: var(--bg); color: var(--ink); font-family: inherit; font-size: 14px; cursor: pointer;">Файл</button>
|
||||
</div>
|
||||
</sc-for>
|
||||
</div>
|
||||
|
||||
<div style="display: grid; gap: 10px; padding: 16px; border-radius: 18px; background: var(--surface); border: 1px solid var(--border);">
|
||||
<div style="display: flex; align-items: baseline; justify-content: space-between; gap: 10px;">
|
||||
<span style="font-size: 16px; font-weight: 500;">Ссылка XRAY</span>
|
||||
<span style="font-size: 12px; color: var(--ink-3);">Happ и другие</span>
|
||||
</div>
|
||||
<div style="padding: 12px; border-radius: 12px; background: var(--surface-2); font-size: 12px; color: var(--ink-2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">{{ curXray }}</div>
|
||||
<div style="display: flex; gap: 8px;">
|
||||
<button type="button" onClick="{{ copyXray }}" style="flex: 1; min-height: 46px; border-radius: 12px; border: 0; background: var(--accent); color: var(--on-accent); font-family: inherit; font-size: 15px; cursor: pointer;">{{ copyLabel }}</button>
|
||||
<button type="button" onClick="{{ showXrayQr }}" style="min-height: 46px; padding: 0 18px; border-radius: 12px; border: 1px solid var(--border); background: transparent; color: var(--ink); font-family: inherit; font-size: 15px; cursor: pointer;">QR</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div hidden="{{ switchOff }}" style="display: grid; gap: 12px;">
|
||||
<button type="button" onClick="{{ goKey }}" style="justify-self: start; min-height: 44px; border: 0; background: transparent; color: var(--tg); font-family: inherit; font-size: 15px; cursor: pointer; padding: 0;">← Подписка</button>
|
||||
<div style="display: grid; gap: 4px; padding: 0 2px 2px;">
|
||||
<div style="font-size: 22px; font-weight: 600; letter-spacing: -0.03em;">Сменить страну</div>
|
||||
<div style="font-size: 13px; line-height: 1.5; color: var(--ink-2);">Все страны входят в подписку, переключение бесплатное и применяется сразу.</div>
|
||||
</div>
|
||||
<sc-for list="{{ pickList }}" as="loc" hint-placeholder-count="8">
|
||||
<button type="button" onClick="{{ loc.pick }}" style="display: flex; align-items: center; gap: 11px; width: 100%; min-height: 44px; padding: 12px 14px; border-radius: 16px; border: 1px solid {{ loc.border }}; background: {{ loc.bg }}; color: var(--ink); font-family: inherit; text-align: left; cursor: pointer;">
|
||||
{{ loc.flag }}
|
||||
<span style="flex: 1; min-width: 0; display: grid; gap: 2px;">
|
||||
<span style="font-size: 15px; font-weight: 500;">{{ loc.country }}</span>
|
||||
<span style="font-size: 12px; color: {{ loc.statusInk }};">{{ loc.sub }}</span>
|
||||
</span>
|
||||
<span style="width: 10px; height: 10px; border-radius: 50%; background: {{ loc.dot }}; display: block; flex: none;"></span>
|
||||
</button>
|
||||
</sc-for>
|
||||
</div>
|
||||
|
||||
<div hidden="{{ moreOff }}" style="display: grid; gap: 12px;">
|
||||
<div style="font-size: 22px; font-weight: 600; letter-spacing: -0.03em; padding: 2px;">Ещё</div>
|
||||
<div style="display: grid; gap: 1px; border-radius: 18px; overflow: hidden; border: 1px solid var(--border); background: var(--border);">
|
||||
<button type="button" onClick="{{ goBilling }}" style="display: flex; align-items: center; gap: 12px; min-height: 54px; padding: 12px 16px; border: 0; background: var(--surface); color: var(--ink); font-family: inherit; font-size: 15px; text-align: left; cursor: pointer;">
|
||||
<span style="flex: 1;">Счёт и платежи</span>
|
||||
<span style="color: var(--ink-3); font-variant-numeric: tabular-nums;">350 ₽</span>
|
||||
<span style="color: var(--ink-3);">›</span>
|
||||
</button>
|
||||
<button type="button" onClick="{{ goRef }}" style="display: flex; align-items: center; gap: 12px; min-height: 54px; padding: 12px 16px; border: 0; background: var(--surface); color: var(--ink); font-family: inherit; font-size: 15px; text-align: left; cursor: pointer;">
|
||||
<span style="flex: 1;">Реферальная программа</span>
|
||||
<span style="color: var(--ink-3);">{{ refStats }}</span>
|
||||
<span style="color: var(--ink-3);">›</span>
|
||||
</button>
|
||||
<a href="Main.dc.html" style="display: flex; align-items: center; gap: 12px; min-height: 54px; padding: 12px 16px; background: var(--surface); color: var(--ink); font-size: 15px;">
|
||||
<span style="flex: 1;">Нужен свой IP-адрес</span>
|
||||
<span style="color: var(--ink-3);">›</span>
|
||||
</a>
|
||||
<button type="button" onClick="{{ goFaq }}" style="display: flex; align-items: center; gap: 12px; min-height: 54px; padding: 12px 16px; border: 0; background: var(--surface); color: var(--ink); font-family: inherit; font-size: 15px; text-align: left; cursor: pointer;">
|
||||
<span style="flex: 1;">Частые вопросы</span>
|
||||
<span style="color: var(--ink-3);">›</span>
|
||||
</button>
|
||||
</div>
|
||||
<div style="display: grid; gap: 1px; border-radius: 18px; overflow: hidden; border: 1px solid var(--border); background: var(--border);">
|
||||
<div style="display: flex; align-items: center; gap: 12px; min-height: 52px; padding: 12px 16px; background: var(--surface); font-size: 15px;">
|
||||
<span style="flex: 1;">Написать в поддержку</span>
|
||||
<span style="color: var(--tg);">@veilix_support</span>
|
||||
</div>
|
||||
<div style="display: flex; align-items: center; gap: 12px; min-height: 52px; padding: 12px 16px; background: var(--surface); font-size: 15px;">
|
||||
<span style="flex: 1;">Почта аккаунта</span>
|
||||
<span style="color: var(--ink-3);">anna@example.com</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: grid; gap: 12px; padding: 14px 16px; border-radius: 18px; background: var(--surface); border: 1px solid var(--border);">
|
||||
<div style="display: flex; align-items: center; justify-content: space-between; gap: 12px;">
|
||||
<span style="font-size: 15px;">Тема</span>
|
||||
<span style="display: flex; flex: none; padding: 3px; border-radius: 11px; border: 1px solid var(--border);">
|
||||
<button type="button" onClick="{{ setLight }}" title="Светлая тема" style="display: flex; align-items: center; justify-content: center; width: 40px; height: 34px; border: 0; border-radius: 8px; background: {{ lightBg }}; color: var(--ink); cursor: pointer;">
|
||||
<span style="position: relative; width: 16px; height: 16px; display: block;">
|
||||
<span style="position: absolute; left: 4px; top: 4px; width: 8px; height: 8px; border-radius: 50%; background: currentColor; display: block;"></span>
|
||||
<span style="position: absolute; left: 0; top: 0; width: 16px; height: 16px; border-radius: 50%; border: 1.5px solid currentColor; opacity: 0.45; display: block;"></span>
|
||||
</span>
|
||||
</button>
|
||||
<button type="button" onClick="{{ setDark }}" title="Тёмная тема" style="display: flex; align-items: center; justify-content: center; width: 40px; height: 34px; border: 0; border-radius: 8px; background: {{ darkBg }}; color: var(--ink); cursor: pointer;">
|
||||
<span style="width: 16px; height: 16px; border-radius: 50%; box-shadow: inset -5px -2px 0 0 currentColor; display: block;"></span>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
<div style="display: flex; align-items: center; justify-content: space-between; gap: 12px; padding-top: 12px; border-top: 1px solid var(--border);">
|
||||
<span style="font-size: 15px;">Язык</span>
|
||||
<span style="display: flex; flex: none; padding: 3px; border-radius: 11px; border: 1px solid var(--border);">
|
||||
<button type="button" onClick="{{ setRu }}" style="height: 34px; padding: 0 16px; border: 0; border-radius: 8px; background: {{ ruBg }}; color: {{ ruInk }}; font-family: inherit; font-size: 14px; cursor: pointer;">RU</button>
|
||||
<button type="button" onClick="{{ setEn }}" style="height: 34px; padding: 0 16px; border: 0; border-radius: 8px; background: {{ enBg }}; color: {{ enInk }}; font-family: inherit; font-size: 14px; cursor: pointer;">EN</button>
|
||||
</span>
|
||||
</div>
|
||||
<div style="font-size: 12px; color: var(--ink-3); line-height: 1.45;">По умолчанию тема подстраивается под Telegram.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div hidden="{{ billingOff }}" style="display: grid; gap: 12px;">
|
||||
<button type="button" onClick="{{ goMore }}" style="justify-self: start; min-height: 44px; border: 0; background: transparent; color: var(--tg); font-family: inherit; font-size: 15px; cursor: pointer; padding: 0;">← Ещё</button>
|
||||
<div style="display: grid; gap: 4px; padding: 0 2px 2px;">
|
||||
<div style="font-size: 22px; font-weight: 600; letter-spacing: -0.03em;">Счёт и платежи</div>
|
||||
<div style="font-size: 13px; line-height: 1.5; color: var(--ink-2);">Карта привязана к аккаунту. Подписка списывается с неё или с баланса, если на нём есть деньги.</div>
|
||||
</div>
|
||||
|
||||
<div style="display: grid; gap: 12px; padding: 16px; border-radius: 18px; background: var(--surface); border: 1px solid var(--border);">
|
||||
<div style="display: grid; gap: 3px;">
|
||||
<span style="font-size: 13px; color: var(--ink-3);">Баланс</span>
|
||||
<span style="font-size: 28px; font-weight: 600; letter-spacing: -0.03em; font-variant-numeric: tabular-nums;">350 ₽</span>
|
||||
</div>
|
||||
<button type="button" onClick="{{ openTopup }}" style="min-height: 48px; border-radius: 12px; border: 0; background: var(--accent); color: var(--on-accent); font-family: inherit; font-size: 15px; font-weight: 500; cursor: pointer;">Пополнить баланс</button>
|
||||
</div>
|
||||
|
||||
<div hidden="{{ topupOff }}" style="display: grid; gap: 12px; padding: 16px; border-radius: 18px; background: var(--surface); border: 1px solid var(--accent);">
|
||||
<div style="display: flex; align-items: baseline; justify-content: space-between; gap: 10px;">
|
||||
<span style="font-size: 16px; font-weight: 500;">Пополнение</span>
|
||||
<button type="button" onClick="{{ closeTopup }}" style="border: 0; background: transparent; color: var(--ink-3); font-family: inherit; font-size: 14px; cursor: pointer; padding: 0;">Свернуть</button>
|
||||
</div>
|
||||
<div style="display: flex; flex-wrap: wrap; gap: 8px;">
|
||||
<sc-for list="{{ sums }}" as="sum" hint-placeholder-count="3">
|
||||
<button type="button" onClick="{{ sum.pick }}" style="min-height: 44px; padding: 0 16px; border-radius: 12px; border: 1px solid {{ sum.border }}; background: {{ sum.bg }}; color: var(--ink); font-family: inherit; font-size: 15px; font-variant-numeric: tabular-nums; cursor: pointer;">{{ sum.label }}</button>
|
||||
</sc-for>
|
||||
</div>
|
||||
<div style="display: grid; gap: 6px;">
|
||||
<sc-for list="{{ methods }}" as="m" hint-placeholder-count="3">
|
||||
<button type="button" onClick="{{ m.pick }}" style="display: flex; align-items: center; gap: 10px; min-height: 48px; padding: 0 14px; border-radius: 12px; border: 1px solid {{ m.border }}; background: {{ m.bg }}; color: var(--ink); font-family: inherit; font-size: 15px; text-align: left; cursor: pointer;">
|
||||
<span style="flex: 1;">{{ m.label }}</span>
|
||||
<span style="font-size: 12px; color: var(--ink-3);">{{ m.note }}</span>
|
||||
</button>
|
||||
</sc-for>
|
||||
</div>
|
||||
<div hidden="{{ cryptoOff }}" style="padding: 12px 14px; border-radius: 12px; background: var(--surface-2); font-size: 13px; line-height: 1.5; color: var(--ink-2);">USDT (TRC-20), BTC, ETH. Покажем адрес и сумму в монете, курс фиксируется на 15 минут. Баланс пополнится после одного подтверждения сети.</div>
|
||||
<button type="button" onClick="{{ sendTopup }}" style="min-height: 48px; border-radius: 12px; border: 0; background: var(--accent); color: var(--on-accent); font-family: inherit; font-size: 15px; font-weight: 500; cursor: pointer;">{{ topupButton }}</button>
|
||||
</div>
|
||||
|
||||
<div style="display: grid; gap: 1px; border-radius: 18px; overflow: hidden; border: 1px solid var(--border); background: var(--border);">
|
||||
<div style="display: flex; align-items: center; gap: 12px; min-height: 52px; padding: 12px 16px; background: var(--surface); font-size: 15px;">
|
||||
<span style="flex: 1;">Карта аккаунта</span>
|
||||
<span style="color: var(--ink-3); font-variant-numeric: tabular-nums;">···· 4242</span>
|
||||
</div>
|
||||
<div style="display: flex; align-items: center; gap: 12px; min-height: 52px; padding: 12px 16px; background: var(--surface); font-size: 15px;">
|
||||
<span style="flex: 1;">Следующее списание</span>
|
||||
<span style="color: var(--ink-3); font-variant-numeric: tabular-nums;">699 ₽ · 14 окт.</span>
|
||||
</div>
|
||||
<div style="display: flex; align-items: center; gap: 12px; min-height: 52px; padding: 12px 16px; background: var(--surface); font-size: 15px;">
|
||||
<span style="flex: 1;">Перейти на год</span>
|
||||
<span style="color: var(--accent);">6 990 ₽</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="font-size: 16px; font-weight: 500; padding: 8px 2px 0;">История платежей</div>
|
||||
<sc-for list="{{ payments }}" as="p" hint-placeholder-count="5">
|
||||
<div style="display: flex; align-items: center; gap: 12px; padding: 14px 16px; border-radius: 16px; background: var(--surface); border: 1px solid var(--border);">
|
||||
<span style="flex: 1; min-width: 0; display: grid; gap: 3px;">
|
||||
<span style="font-size: 14px;">{{ p.title }}</span>
|
||||
<span style="font-size: 12px; color: var(--ink-3);">{{ p.meta }}</span>
|
||||
</span>
|
||||
<span style="flex: none; font-size: 15px; font-weight: 500; color: {{ p.ink }}; font-variant-numeric: tabular-nums;">{{ p.amount }}</span>
|
||||
</div>
|
||||
</sc-for>
|
||||
</div>
|
||||
|
||||
<div hidden="{{ refOff }}" style="display: grid; gap: 12px;">
|
||||
<button type="button" onClick="{{ goMore }}" style="justify-self: start; min-height: 44px; border: 0; background: transparent; color: var(--tg); font-family: inherit; font-size: 15px; cursor: pointer; padding: 0;">← Ещё</button>
|
||||
<div style="display: grid; gap: 4px; padding: 0 2px 2px;">
|
||||
<div style="font-size: 22px; font-weight: 600; letter-spacing: -0.03em;">Реферальная программа</div>
|
||||
<div style="font-size: 13px; line-height: 1.5; color: var(--ink-2);">Приведите друга — вы оба получите по месяцу в подарок.</div>
|
||||
</div>
|
||||
<div style="display: flex; gap: 8px;">
|
||||
<div style="flex: 1; display: grid; gap: 4px; padding: 14px; border-radius: 16px; background: var(--surface); border: 1px solid var(--border);">
|
||||
<span style="font-size: 12px; color: var(--ink-3);">Приглашено</span>
|
||||
<span style="font-size: 24px; font-weight: 600; letter-spacing: -0.03em; font-variant-numeric: tabular-nums;">3</span>
|
||||
</div>
|
||||
<div style="flex: 1; display: grid; gap: 4px; padding: 14px; border-radius: 16px; background: var(--surface); border: 1px solid var(--border);">
|
||||
<span style="font-size: 12px; color: var(--ink-3);">Месяцев</span>
|
||||
<span style="font-size: 24px; font-weight: 600; letter-spacing: -0.03em; font-variant-numeric: tabular-nums;">2</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: grid; gap: 10px; padding: 16px; border-radius: 18px; background: var(--surface); border: 1px solid var(--border);">
|
||||
<div style="font-size: 15px; font-weight: 500;">Ваша ссылка</div>
|
||||
<div style="padding: 12px; border-radius: 12px; background: var(--surface-2); font-size: 13px; color: var(--ink-2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">t.me/veilixvpn_bot?start=anna7f2c</div>
|
||||
<div style="display: flex; gap: 8px;">
|
||||
<button type="button" onClick="{{ copyRef }}" style="flex: 1; min-height: 46px; border-radius: 12px; border: 0; background: var(--accent); color: var(--on-accent); font-family: inherit; font-size: 15px; cursor: pointer;">{{ refLabel }}</button>
|
||||
<button type="button" style="min-height: 46px; padding: 0 18px; border-radius: 12px; border: 1px solid var(--border); background: transparent; color: var(--ink); font-family: inherit; font-size: 15px; cursor: pointer;">Поделиться</button>
|
||||
</div>
|
||||
<div style="font-size: 12px; line-height: 1.45; color: var(--ink-3);">Месяц начисляется после первой оплаты друга и применяется к ближайшему списанию.</div>
|
||||
</div>
|
||||
<div style="display: grid; gap: 1px; border-radius: 18px; overflow: hidden; border: 1px solid var(--border); background: var(--border);">
|
||||
<div style="display: flex; align-items: center; gap: 12px; min-height: 50px; padding: 12px 16px; background: var(--surface); font-size: 14px;">
|
||||
<span style="flex: 1;">m···@gmail.com</span>
|
||||
<span style="color: var(--accent);">Месяц начислен</span>
|
||||
</div>
|
||||
<div style="display: flex; align-items: center; gap: 12px; min-height: 50px; padding: 12px 16px; background: var(--surface); font-size: 14px;">
|
||||
<span style="flex: 1;">d···@yandex.ru</span>
|
||||
<span style="color: var(--accent);">Месяц начислен</span>
|
||||
</div>
|
||||
<div style="display: flex; align-items: center; gap: 12px; min-height: 50px; padding: 12px 16px; background: var(--surface); font-size: 14px;">
|
||||
<span style="flex: 1;">k···@icloud.com</span>
|
||||
<span style="color: var(--ink-3);">Ждём оплаты</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="position: absolute; left: 50%; bottom: 46px; transform: translateX(-50%); z-index: 15; display: flex; gap: 2px; padding: 6px; border-radius: 999px; background: color-mix(in oklab, var(--surface) 62%, transparent); backdrop-filter: blur(24px) saturate(1.8); -webkit-backdrop-filter: blur(24px) saturate(1.8); border: 1px solid color-mix(in oklab, var(--ink) 8%, transparent); box-shadow: 0 10px 30px -10px color-mix(in oklab, var(--ink) 40%, transparent);">
|
||||
<button type="button" onClick="{{ goBuy }}" style="display: grid; justify-items: center; gap: 4px; min-width: 72px; min-height: 52px; padding: 8px 10px; border: 0; border-radius: 999px; background: {{ tabBuyBg }}; color: {{ tabBuyInk }}; font-family: inherit; font-size: 11px; cursor: pointer;">
|
||||
<span style="position: relative; width: 20px; height: 20px; display: block;">
|
||||
<span style="position: absolute; left: 8px; top: 0; width: 4px; height: 20px; border-radius: 2px; background: currentColor; display: block;"></span>
|
||||
<span style="position: absolute; left: 0; top: 8px; width: 20px; height: 4px; border-radius: 2px; background: currentColor; display: block;"></span>
|
||||
</span>
|
||||
Купить
|
||||
</button>
|
||||
<button type="button" onClick="{{ goFaq }}" style="display: grid; justify-items: center; gap: 4px; min-width: 72px; min-height: 52px; padding: 8px 10px; border: 0; border-radius: 999px; background: {{ tabFaqBg }}; color: {{ tabFaqInk }}; font-family: inherit; font-size: 11px; cursor: pointer;">
|
||||
<span style="display: flex; align-items: center; justify-content: center; width: 20px; height: 20px; border-radius: 50%; border: 2px solid currentColor; font-size: 13px; font-weight: 600; line-height: 1;">?</span>
|
||||
FAQ
|
||||
</button>
|
||||
<button type="button" onClick="{{ goKeys }}" style="display: grid; justify-items: center; gap: 4px; min-width: 72px; min-height: 52px; padding: 8px 10px; border: 0; border-radius: 999px; background: {{ tabKeysBg }}; color: {{ tabKeysInk }}; font-family: inherit; font-size: 11px; cursor: pointer;">
|
||||
<span style="position: relative; width: 20px; height: 20px; display: block;">
|
||||
<span style="position: absolute; left: 0; top: 2px; width: 20px; height: 4px; border-radius: 2px; background: currentColor; display: block;"></span>
|
||||
<span style="position: absolute; left: 0; top: 9px; width: 20px; height: 4px; border-radius: 2px; background: currentColor; opacity: 0.6; display: block;"></span>
|
||||
<span style="position: absolute; left: 0; top: 16px; width: 20px; height: 4px; border-radius: 2px; background: currentColor; opacity: 0.3; display: block;"></span>
|
||||
</span>
|
||||
Подписка
|
||||
</button>
|
||||
<button type="button" onClick="{{ goMore }}" style="display: grid; justify-items: center; gap: 4px; min-width: 72px; min-height: 52px; padding: 8px 10px; border: 0; border-radius: 999px; background: {{ tabMoreBg }}; color: {{ tabMoreInk }}; font-family: inherit; font-size: 11px; cursor: pointer;">
|
||||
<span style="position: relative; width: 20px; height: 20px; display: block;">
|
||||
<span style="position: absolute; left: 0; top: 8px; width: 4px; height: 4px; border-radius: 50%; background: currentColor; display: block;"></span>
|
||||
<span style="position: absolute; left: 8px; top: 8px; width: 4px; height: 4px; border-radius: 50%; background: currentColor; display: block;"></span>
|
||||
<span style="position: absolute; left: 16px; top: 8px; width: 4px; height: 4px; border-radius: 50%; background: currentColor; display: block;"></span>
|
||||
</span>
|
||||
Ещё
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div hidden="{{ qrOff }}" onClick="{{ closeQr }}" style="position: absolute; left: 0; right: 0; top: -62px; bottom: -34px; z-index: 20; display: flex; align-items: flex-end; justify-content: center; padding-bottom: 34px; background: color-mix(in oklab, var(--ink) 45%, transparent);">
|
||||
<div style="width: 100%; display: grid; gap: 14px; justify-items: center; padding: 22px 20px 34px; border-radius: 22px 22px 0 0; background: var(--surface); border-top: 1px solid var(--border);">
|
||||
<span style="width: 36px; height: 4px; border-radius: 2px; background: var(--border); display: block;"></span>
|
||||
<span style="font-size: 16px; font-weight: 500;">{{ qrTitle }}</span>
|
||||
{{ qrImage }}
|
||||
<span style="font-size: 12px; color: var(--ink-3); text-align: center; max-width: 240px; line-height: 1.45;">Наведите камеру телефона на код в приложении Happ или другом клиенте XRAY.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</x-import>
|
||||
|
||||
<div style="max-width: 320px; display: grid; gap: 14px; align-content: start; padding-top: 12px;">
|
||||
<div style="font-size: 20px; font-weight: 600; letter-spacing: -0.03em; color: var(--ink);">Мини-приложение в Telegram</div>
|
||||
<p style="margin: 0; font-size: 15px; line-height: 1.55; color: var(--ink-2); font-weight: 300;">В Telegram приложение работает и как витрина, и как кабинет: человек может попасть сюда, ни разу не открыв сайт. Первая вкладка — «Купить»: тезисы, месяц и год, все локации подписки и переход на индивидуальный тариф.</p>
|
||||
<p style="margin: 0; font-size: 14px; line-height: 1.55; color: var(--ink-3); font-weight: 300;">Вкладки: Купить, FAQ, Подписка, Ещё. В «Ещё» — счёт и платежи, реферальная программа, свой IP-адрес, тема и язык.</p>
|
||||
</div>
|
||||
</div>
|
||||
</x-dc>
|
||||
<script type="text/x-dc" data-dc-script data-props="{"defaultTheme":{"editor":"enum","options":["light","dark","system"],"default":"light","tsType":"'light' | 'dark' | 'system'","section":"Тема"}}">
|
||||
class Component extends DCLogic {
|
||||
state = { theme: null, sysDark: false, view: "buy", key: 0, locs: [0, 19], loc: 0, qr: null, copied: false, lang: "ru", refCopied: false, faq: -1, plan: 0, paid: false, topup: false, sum: 1, method: 0, topupSent: false };
|
||||
|
||||
componentDidMount() {
|
||||
const mq = window.matchMedia("(prefers-color-scheme: dark)");
|
||||
this.mq = mq;
|
||||
this.onSys = () => this.setState({ sysDark: mq.matches });
|
||||
mq.addEventListener("change", this.onSys);
|
||||
let saved = null;
|
||||
try { saved = localStorage.getItem("veilix.theme"); } catch (e) {}
|
||||
let savedLang = null;
|
||||
try { savedLang = localStorage.getItem("veilix.lang"); } catch (e) {}
|
||||
this.setState({ sysDark: mq.matches, theme: saved || this.props.defaultTheme || "light", lang: savedLang || "ru" }, () => { this.apply(); this.applyLang(); });
|
||||
}
|
||||
|
||||
applyLang() {
|
||||
const lang = this.state.lang || "ru";
|
||||
if (window.VEILIX_APPLY_LANG) window.VEILIX_APPLY_LANG(document.body, lang);
|
||||
else setTimeout(() => this.applyLang(), 150);
|
||||
}
|
||||
|
||||
setLang(l) {
|
||||
try { localStorage.setItem("veilix.lang", l); } catch (e) {}
|
||||
this.setState({ lang: l }, () => this.applyLang());
|
||||
}
|
||||
|
||||
componentWillUnmount() { if (this.mq) this.mq.removeEventListener("change", this.onSys); }
|
||||
componentDidUpdate() { this.apply(); this.applyLang(); }
|
||||
|
||||
apply() {
|
||||
const theme = this.state.theme || this.props.defaultTheme || "light";
|
||||
const dark = theme === "dark" || (theme === "system" && this.state.sysDark);
|
||||
document.documentElement.setAttribute("data-veilix-theme", dark ? "dark" : "light");
|
||||
}
|
||||
|
||||
setTheme(t) {
|
||||
try { localStorage.setItem("veilix.theme", t); } catch (e) {}
|
||||
this.setState({ theme: t }, () => this.apply());
|
||||
}
|
||||
|
||||
data() {
|
||||
const L = (country, city, code, ready) => ({ country, city, code, ready });
|
||||
return [
|
||||
L("Нидерланды", "Амстердам", "nl", true),
|
||||
L("Германия", "Франкфурт", "de", true),
|
||||
L("США", "Нью-Йорк", "us", true),
|
||||
L("Великобритания", "Лондон", "gb", true),
|
||||
L("Франция", "Париж", "fr", true),
|
||||
L("Финляндия", "Хельсинки", "fi", true),
|
||||
L("Швеция", "Стокгольм", "se", true),
|
||||
L("Норвегия", "Осло", "no", false),
|
||||
L("Дания", "Копенгаген", "dk", false),
|
||||
L("Швейцария", "Цюрих", "ch", true),
|
||||
L("Австрия", "Вена", "at", true),
|
||||
L("Испания", "Мадрид", "es", true),
|
||||
L("Италия", "Милан", "it", true),
|
||||
L("Польша", "Варшава", "pl", true),
|
||||
L("Чехия", "Прага", "cz", true),
|
||||
L("Румыния", "Бухарест", "ro", false),
|
||||
L("Литва", "Вильнюс", "lt", true),
|
||||
L("Латвия", "Рига", "lv", true),
|
||||
L("Эстония", "Таллин", "ee", false),
|
||||
L("Турция", "Стамбул", "tr", true),
|
||||
L("Казахстан", "Алматы", "kz", false),
|
||||
L("Армения", "Ереван", "am", false),
|
||||
L("Грузия", "Тбилиси", "ge", false),
|
||||
L("ОАЭ", "Дубай", "ae", true),
|
||||
L("Сингапур", "Сингапур", "sg", true),
|
||||
L("Япония", "Токио", "jp", true),
|
||||
L("Южная Корея", "Сеул", "kr", false),
|
||||
L("Канада", "Торонто", "ca", true),
|
||||
L("Австралия", "Сидней", "au", false),
|
||||
L("Бразилия", "Сан-Паулу", "br", false)
|
||||
];
|
||||
}
|
||||
|
||||
flagSpecs() {
|
||||
return {
|
||||
nl: { rows: ["#AE1C28", "#FFFFFF", "#21468B"] },
|
||||
de: { rows: ["#000000", "#DD0000", "#FFCE00"] },
|
||||
us: { rows: ["#B22234", "#FFFFFF", "#B22234", "#FFFFFF", "#B22234", "#FFFFFF"], marks: [{ l: 0, t: 0, w: 19, h: 15, bg: "#3C3B6E" }] },
|
||||
gb: { bg: "#012169", marks: [{ l: 15, t: 0, w: 10, h: 28, bg: "#FFFFFF" }, { l: 0, t: 9, w: 40, h: 10, bg: "#FFFFFF" }, { l: 17, t: 0, w: 5, h: 28, bg: "#C8102E" }, { l: 0, t: 11, w: 40, h: 5, bg: "#C8102E" }] },
|
||||
fr: { cols: ["#002395", "#FFFFFF", "#ED2939"] },
|
||||
fi: { bg: "#FFFFFF", marks: [{ l: 11, t: 0, w: 7, h: 28, bg: "#003580" }, { l: 0, t: 10, w: 40, h: 7, bg: "#003580" }] },
|
||||
se: { bg: "#006AA7", marks: [{ l: 11, t: 0, w: 5, h: 28, bg: "#FECC00" }, { l: 0, t: 11, w: 40, h: 5, bg: "#FECC00" }] },
|
||||
no: { bg: "#BA0C2F", marks: [{ l: 10, t: 0, w: 9, h: 28, bg: "#FFFFFF" }, { l: 0, t: 9, w: 40, h: 9, bg: "#FFFFFF" }, { l: 13, t: 0, w: 3, h: 28, bg: "#00205B" }, { l: 0, t: 12, w: 40, h: 3, bg: "#00205B" }] },
|
||||
dk: { bg: "#C8102E", marks: [{ l: 11, t: 0, w: 5, h: 28, bg: "#FFFFFF" }, { l: 0, t: 11, w: 40, h: 5, bg: "#FFFFFF" }] },
|
||||
ch: { bg: "#D52B1E", marks: [{ l: 17, t: 7, w: 5, h: 14, bg: "#FFFFFF" }, { l: 13, t: 11, w: 14, h: 5, bg: "#FFFFFF" }] },
|
||||
at: { rows: ["#ED2939", "#FFFFFF", "#ED2939"] },
|
||||
es: { rows: ["#AA151B", "#F1BF00", "#AA151B"], sizes: "1fr 2fr 1fr" },
|
||||
it: { cols: ["#008C45", "#F4F5F0", "#CD212A"] },
|
||||
pl: { rows: ["#FFFFFF", "#DC143C"] },
|
||||
cz: { rows: ["#FFFFFF", "#D7141A"], marks: [{ l: 0, t: 0, w: 16, h: 28, bg: "#11457E", clip: "polygon(0 0, 100% 50%, 0 100%)" }] },
|
||||
ro: { cols: ["#002B7F", "#FCD116", "#CE1126"] },
|
||||
lt: { rows: ["#FDB913", "#006A44", "#C1272D"] },
|
||||
lv: { rows: ["#9E3039", "#FFFFFF", "#9E3039"], sizes: "2fr 1fr 2fr" },
|
||||
ee: { rows: ["#0072CE", "#000000", "#FFFFFF"] },
|
||||
tr: { bg: "#E30A17", marks: [{ l: 9, t: 7, w: 14, h: 14, r: "50%", bg: "#FFFFFF" }, { l: 13, t: 7, w: 14, h: 14, r: "50%", bg: "#E30A17" }] },
|
||||
kz: { bg: "#00AFCA", marks: [{ l: 14, t: 7, w: 13, h: 13, r: "50%", bg: "#FEC50C" }] },
|
||||
am: { rows: ["#D90012", "#0033A0", "#F2A800"] },
|
||||
ge: { bg: "#FFFFFF", marks: [{ l: 17, t: 0, w: 5, h: 28, bg: "#FF0000" }, { l: 0, t: 11, w: 40, h: 5, bg: "#FF0000" }] },
|
||||
ae: { rows: ["#00732F", "#FFFFFF", "#000000"], marks: [{ l: 0, t: 0, w: 10, h: 28, bg: "#FF0000" }] },
|
||||
sg: { rows: ["#EF3340", "#FFFFFF"], marks: [{ l: 4, t: 3, w: 10, h: 10, r: "50%", bg: "#FFFFFF" }, { l: 8, t: 3, w: 10, h: 10, r: "50%", bg: "#EF3340" }] },
|
||||
jp: { bg: "#FFFFFF", marks: [{ l: 14, t: 8, w: 12, h: 12, r: "50%", bg: "#BC002D" }] },
|
||||
kr: { bg: "#FFFFFF", marks: [{ l: 14, t: 8, w: 12, h: 12, r: "50%", bg: "#CD2E3A" }, { l: 14, t: 8, w: 12, h: 12, r: "50%", bg: "#0047A0", clip: "polygon(0 50%, 100% 50%, 100% 100%, 0 100%)" }] },
|
||||
ca: { cols: ["#D80621", "#FFFFFF", "#D80621"], sizes: "1fr 2fr 1fr", marks: [{ l: 17, t: 8, w: 6, h: 12, bg: "#D80621", clip: "polygon(50% 0, 100% 100%, 0 100%)" }] },
|
||||
au: { bg: "#00247D", marks: [{ l: 8, t: 0, w: 3, h: 14, bg: "#FFFFFF" }, { l: 0, t: 5, w: 19, h: 3, bg: "#FFFFFF" }, { l: 26, t: 17, w: 6, h: 6, r: "50%", bg: "#FFFFFF" }] },
|
||||
br: { bg: "#009739", marks: [{ l: 8, t: 3, w: 24, h: 22, bg: "#FEDD00", clip: "polygon(50% 0, 100% 50%, 50% 100%, 0 50%)" }, { l: 15, t: 10, w: 9, h: 9, r: "50%", bg: "#012169" }] }
|
||||
};
|
||||
}
|
||||
|
||||
flag(code) {
|
||||
const h = React.createElement;
|
||||
const spec = this.flagSpecs()[code] || {};
|
||||
const style = { width: "40px", height: "28px", borderRadius: "5px", overflow: "hidden", display: "grid", border: "1px solid var(--border)", position: "relative", flex: "none" };
|
||||
let bands = [];
|
||||
if (spec.rows) {
|
||||
bands = spec.rows;
|
||||
style.gridTemplateRows = spec.sizes || "repeat(" + bands.length + ", 1fr)";
|
||||
} else if (spec.cols) {
|
||||
bands = spec.cols;
|
||||
style.gridAutoFlow = "column";
|
||||
style.gridTemplateColumns = spec.sizes || "repeat(" + bands.length + ", 1fr)";
|
||||
} else {
|
||||
style.background = spec.bg || "var(--surface-2)";
|
||||
}
|
||||
const kids = bands.map((c, i) => h("span", { key: "b" + i, style: { background: c, display: "block" } }));
|
||||
(spec.marks || []).forEach((m, i) => kids.push(h("span", {
|
||||
key: "m" + i,
|
||||
style: { position: "absolute", left: m.l + "px", top: m.t + "px", width: m.w + "px", height: m.h + "px", borderRadius: m.r || 0, background: m.bg, clipPath: m.clip || "none", display: "block" }
|
||||
})));
|
||||
return h("span", { style: style }, kids);
|
||||
}
|
||||
|
||||
qrImage(seed) {
|
||||
const n = 21;
|
||||
let h = 0;
|
||||
for (let i = 0; i < seed.length; i++) h = (h * 31 + seed.charCodeAt(i)) >>> 0;
|
||||
const cells = [];
|
||||
const finder = (r, c) => (r < 7 && c < 7) || (r < 7 && c >= n - 7) || (r >= n - 7 && c < 7);
|
||||
for (let r = 0; r < n; r++) {
|
||||
for (let c = 0; c < n; c++) {
|
||||
let on;
|
||||
if (finder(r, c)) {
|
||||
const rr = r < 7 ? r : n - 1 - r;
|
||||
const cc = c < 7 ? c : n - 1 - c;
|
||||
on = Math.max(Math.abs(rr - 3), Math.abs(cc - 3)) !== 2;
|
||||
} else {
|
||||
h = (h * 1103515245 + 12345) >>> 0;
|
||||
on = ((h >>> 16) & 1) === 1;
|
||||
}
|
||||
cells.push(React.createElement("span", { key: r + "-" + c, style: { background: on ? "var(--ink)" : "transparent", display: "block" } }));
|
||||
}
|
||||
}
|
||||
return React.createElement("div", {
|
||||
style: { display: "grid", gridTemplateColumns: "repeat(" + n + ", 1fr)", width: "196px", height: "196px", padding: "10px", borderRadius: "14px", background: "var(--bg)", border: "1px solid var(--border)" }
|
||||
}, cells);
|
||||
}
|
||||
|
||||
renderVals() {
|
||||
const s = this.state;
|
||||
const sel = "var(--sel)";
|
||||
const theme = s.theme || this.props.defaultTheme || "light";
|
||||
const all = this.data();
|
||||
const keyData = [
|
||||
{ title: "Основная подписка", plan: "699 ₽ в месяц", devices: "3 из 5 устройств", until: s.lang === "en" ? "14 October" : "14 октября", badge: "Активна", warn: false, left: "Продлится автоматически через 24 дня" },
|
||||
{ title: "Вторая подписка", plan: "6 990 ₽ в год", devices: "2 из 5 устройств", until: s.lang === "en" ? "21 September" : "21 сентября", badge: "Истекает", warn: true, left: "Осталось 8 дней" }
|
||||
];
|
||||
const locs = s.locs || [0, 19];
|
||||
const k = keyData[s.key] || keyData[0];
|
||||
const cur = all[locs[s.key] || 0] || all[0];
|
||||
const cfgNames = [["Ноутбук", "подключено"], ["Телефон", "подключено"], ["Планшет", "подключено"], ["Рабочий ноутбук", "не занят"], ["Свободный слот", "не занят"]];
|
||||
const faqData = [
|
||||
["Что входит в подписку?", "Все тридцать локаций, включая российскую, пять устройств и протокол XRAY. Отдельной платы за страну нет, переключение бесплатное."],
|
||||
["Можно менять страну сколько угодно?", "Да. Переключение занимает пару секунд и ничего не стоит: список локаций открыт целиком на всё время подписки."],
|
||||
["Сколько устройств можно подключить?", "До пяти одновременно. Каждое может быть подключено к своей стране."],
|
||||
["На каком протоколе работает подключение?", "На XRAY — VLESS, Hysteria2 и XHTTP. Трафик неотличим от обычного HTTPS, поэтому подключение стабильно работает там, где старые протоколы блокируют."],
|
||||
["Чем это отличается от индивидуального IP?", "Здесь сервер общий: он дешевле и этого достаточно большинству. Если сервисы требуют постоянный адрес, посмотрите тариф с индивидуальным IP."],
|
||||
["Как платить и как отменить?", "Карта привязана к аккаунту, можно держать баланс и пополнять его картой, через СБП или криптой. Отмена — в разделе «Счёт и платежи», подписка доработает оплаченный период."]
|
||||
];
|
||||
|
||||
return {
|
||||
locCount: s.lang === "en" ? "30 countries" : "30 стран",
|
||||
locations: all.map(d => ({
|
||||
country: d.country,
|
||||
city: d.city,
|
||||
flag: this.flag(d.code),
|
||||
status: d.ready ? "В подписке" : "Скоро",
|
||||
statusInk: d.ready ? "var(--accent)" : "var(--ink-3)"
|
||||
})),
|
||||
pickList: all.map((d, i) => ({
|
||||
country: d.country,
|
||||
flag: this.flag(d.code),
|
||||
sub: d.ready ? d.city + " · доступно" : d.city + " · скоро",
|
||||
statusInk: d.ready ? "var(--accent)" : "var(--ink-3)",
|
||||
bg: locs[s.key] === i ? sel : "var(--surface)",
|
||||
border: locs[s.key] === i ? "var(--accent)" : "var(--border)",
|
||||
dot: locs[s.key] === i ? "var(--accent)" : "transparent",
|
||||
pick: () => this.setState(st => {
|
||||
const next = (st.locs || [0, 19]).slice();
|
||||
next[st.key] = i;
|
||||
return { locs: next, view: "key" };
|
||||
})
|
||||
})),
|
||||
keys: keyData.map((kk, i) => ({
|
||||
title: kk.title,
|
||||
flag: this.flag(all[locs[i] || 0].code),
|
||||
devices: kk.devices,
|
||||
untilLine: (s.lang === "en" ? "until " : "до ") + kk.until,
|
||||
badge: kk.badge,
|
||||
badgeBg: kk.warn ? "color-mix(in oklab, var(--warn) 18%, transparent)" : sel,
|
||||
badgeInk: kk.warn ? "var(--warn)" : "var(--accent)",
|
||||
open: () => this.setState({ view: "key", key: i })
|
||||
})),
|
||||
curTitle: k.title,
|
||||
curPlan: k.plan,
|
||||
curDevices: k.devices,
|
||||
curUntil: k.until,
|
||||
curLeft: k.left,
|
||||
curLeftInk: k.warn ? "var(--warn)" : "var(--ink-2)",
|
||||
keyOff: s.view !== "key",
|
||||
curFlag: this.flag(cur.code),
|
||||
curCountry: cur.country + " · " + cur.city,
|
||||
curXray: "vless://8f2a…@" + cur.code + "1.veilixvpn.net:443?type=xhttp#VeilixVPN",
|
||||
plans: [
|
||||
{ name: "Месяц", price: "699 ₽", note: "списание раз в месяц", accent: false },
|
||||
{ name: "Год", price: "6 990 ₽", note: "два месяца в подарок", accent: true }
|
||||
].map((p, i) => ({
|
||||
name: p.name,
|
||||
price: p.price,
|
||||
note: p.note,
|
||||
noteInk: p.accent ? "var(--accent)" : "var(--ink-3)",
|
||||
bg: s.plan === i ? sel : "var(--surface)",
|
||||
border: s.plan === i ? "var(--accent)" : "var(--border)",
|
||||
pick: () => this.setState({ plan: i, paid: false })
|
||||
})),
|
||||
payButton: s.paid ? "Оплата подтверждена" : (s.plan === 1 ? "Оплатить 6 990 ₽" : "Оплатить 699 ₽"),
|
||||
confirmPay: () => this.setState({ paid: true }),
|
||||
faq: faqData.map((row, i) => ({
|
||||
q: row[0],
|
||||
a: row[1],
|
||||
off: s.faq !== i,
|
||||
rot: s.faq === i ? "rotate(-135deg) translateY(2px)" : "rotate(45deg) translateY(-2px)",
|
||||
toggle: () => this.setState(st => ({ faq: st.faq === i ? -1 : i }))
|
||||
})),
|
||||
configs: cfgNames.map(c => ({
|
||||
name: c[0],
|
||||
state: c[1],
|
||||
showQr: () => this.setState({ qr: { title: "XRAY · " + c[0], seed: cur.code + c[0] } })
|
||||
})),
|
||||
goBuy: () => this.setState({ view: "buy" }),
|
||||
goFaq: () => this.setState({ view: "faq" }),
|
||||
goKeys: () => this.setState({ view: "keys" }),
|
||||
goSwitch: () => this.setState({ view: "switch" }),
|
||||
goMore: () => this.setState({ view: "more" }),
|
||||
goRef: () => this.setState({ view: "ref" }),
|
||||
goBilling: () => this.setState({ view: "billing" }),
|
||||
buyOff: s.view !== "buy",
|
||||
faqOff: s.view !== "faq",
|
||||
keysOff: s.view !== "keys",
|
||||
switchOff: s.view !== "switch",
|
||||
moreOff: s.view !== "more",
|
||||
refOff: s.view !== "ref",
|
||||
billingOff: s.view !== "billing",
|
||||
tabBuyBg: s.view === "buy" ? sel : "transparent",
|
||||
tabFaqBg: s.view === "faq" ? sel : "transparent",
|
||||
goKey: () => this.setState({ view: "key" }),
|
||||
tabKeysBg: (s.view === "keys" || s.view === "key" || s.view === "switch") ? sel : "transparent",
|
||||
tabMoreBg: (s.view === "more" || s.view === "ref" || s.view === "billing") ? sel : "transparent",
|
||||
tabBuyInk: s.view === "buy" ? "var(--accent)" : "var(--ink-2)",
|
||||
tabFaqInk: s.view === "faq" ? "var(--accent)" : "var(--ink-2)",
|
||||
tabKeysInk: (s.view === "keys" || s.view === "key" || s.view === "switch") ? "var(--accent)" : "var(--ink-2)",
|
||||
tabMoreInk: (s.view === "more" || s.view === "ref" || s.view === "billing") ? "var(--accent)" : "var(--ink-2)",
|
||||
openTopup: () => this.setState({ topup: true }),
|
||||
closeTopup: () => this.setState({ topup: false }),
|
||||
topupOff: !s.topup,
|
||||
sums: ["700 ₽", "2 100 ₽", "6 990 ₽"].map((label, i) => ({
|
||||
label: label,
|
||||
bg: s.sum === i ? sel : "transparent",
|
||||
border: s.sum === i ? "var(--accent)" : "var(--border)",
|
||||
pick: () => this.setState({ sum: i })
|
||||
})),
|
||||
methods: [
|
||||
{ label: "Карта ···· 4242", note: "мгновенно" },
|
||||
{ label: "СБП", note: "мгновенно" },
|
||||
{ label: "Криптовалюта", note: "USDT, BTC, ETH" }
|
||||
].map((m, i) => ({
|
||||
label: m.label,
|
||||
note: m.note,
|
||||
bg: s.method === i ? sel : "transparent",
|
||||
border: s.method === i ? "var(--accent)" : "var(--border)",
|
||||
pick: () => this.setState({ method: i })
|
||||
})),
|
||||
cryptoOff: s.method !== 2,
|
||||
sendTopup: () => this.setState({ topupSent: true }),
|
||||
topupButton: s.topupSent ? "Счёт выставлен" : "Пополнить",
|
||||
payments: [
|
||||
{ title: "Подписка на месяц", meta: "14 сентября · карта ···· 4242", amount: "−699 ₽", ink: "var(--ink)" },
|
||||
{ title: "Пополнение баланса", meta: "6 сентября · USDT (TRC-20)", amount: "+2 100 ₽", ink: "var(--accent)" },
|
||||
{ title: "Подписка на месяц", meta: "14 августа · с баланса", amount: "−699 ₽", ink: "var(--ink)" },
|
||||
{ title: "Подписка на месяц", meta: "14 июля · карта ···· 4242", amount: "−699 ₽", ink: "var(--ink)" },
|
||||
{ title: "Реферальный бонус", meta: "2 июля · месяц в подарок", amount: "+699 ₽", ink: "var(--accent)" }
|
||||
],
|
||||
refStats: s.lang === "en" ? "3 invited" : "3 друга",
|
||||
copyRef: () => this.setState({ refCopied: true }),
|
||||
refLabel: s.refCopied ? "Ссылка скопирована" : "Копировать ссылку",
|
||||
copyXray: () => this.setState({ copied: true }),
|
||||
copyLabel: s.copied ? "Скопировано" : "Копировать ссылку",
|
||||
showXrayQr: () => this.setState({ qr: { title: "XRAY · Happ", seed: cur.code + "xray" } }),
|
||||
qrOff: !s.qr,
|
||||
qrTitle: s.qr ? s.qr.title : "",
|
||||
qrImage: s.qr ? this.qrImage(s.qr.seed) : null,
|
||||
closeQr: () => this.setState({ qr: null }),
|
||||
setRu: () => this.setLang("ru"),
|
||||
setEn: () => this.setLang("en"),
|
||||
ruBg: s.lang === "en" ? "transparent" : sel,
|
||||
enBg: s.lang === "en" ? sel : "transparent",
|
||||
ruInk: s.lang === "en" ? "var(--ink-3)" : "var(--ink)",
|
||||
enInk: s.lang === "en" ? "var(--ink)" : "var(--ink-3)",
|
||||
setLight: () => this.setTheme("light"),
|
||||
setDark: () => this.setTheme("dark"),
|
||||
lightBg: theme === "light" ? sel : "transparent",
|
||||
darkBg: theme === "dark" ? sel : "transparent"
|
||||
};
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,602 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<script src="./support.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<x-dc>
|
||||
<helmet>
|
||||
<link rel="icon" type="image/svg+xml" href="favicon-mass.svg" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin="anonymous" />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Commissioner:wght@300;400;500;600&family=Unbounded:wght@500;600&display=swap" rel="stylesheet" />
|
||||
<script>(function(){if(window.__veilixI18nSrc)return;window.__veilixI18nSrc="./i18n.js?v="+Date.now().toString(36)+Math.random().toString(36).slice(2,8);var s=document.createElement("script");s.src=window.__veilixI18nSrc;document.head.appendChild(s);})();</script>
|
||||
<script src="https://unpkg.com/d3@7.9.0/dist/d3.min.js" integrity="sha384-CjloA8y00+1SDAUkjs099PVfnY2KmDC2BZnws9kh8D/lX1s46w6EPhpXdqMfjK6i" crossorigin="anonymous"></script>
|
||||
<script src="https://unpkg.com/topojson-client@3.1.0/dist/topojson-client.min.js" integrity="sha384-Ukv1p/xTma6P4/2bY5KzWBw+ydSpXmhCMtyciIQVDJ1RmOxtCYNMF1uXT9T63H67" crossorigin="anonymous"></script>
|
||||
<style>
|
||||
:root{
|
||||
--bg: oklch(99.6% 0.0015 215);
|
||||
--surface: oklch(100% 0 0);
|
||||
--surface-2: oklch(98.4% 0.003 215);
|
||||
--ink: oklch(24% 0.04 204);
|
||||
--ink-2: oklch(45% 0.03 204);
|
||||
--ink-3: oklch(60% 0.025 204);
|
||||
--border: oklch(92% 0.014 200);
|
||||
--accent: oklch(48% 0.105 178);
|
||||
--accent-hover: oklch(41% 0.1 178);
|
||||
--accent-soft: oklch(95.5% 0.032 180);
|
||||
--sel: oklch(96.5% 0.016 190);
|
||||
--on-accent: oklch(99% 0.01 180);
|
||||
--glow-1: oklch(86% 0.085 190);
|
||||
--glow-2: oklch(88% 0.075 205);
|
||||
--glow-3: oklch(89% 0.07 172);
|
||||
--veil: oklch(100% 0 0 / 0.55);
|
||||
--land: oklch(95% 0.012 196);
|
||||
--land-line: oklch(90% 0.018 196);
|
||||
--shadow: 0 1px 2px oklch(24% 0.04 204 / 0.05), 0 12px 32px -12px oklch(24% 0.04 204 / 0.14);
|
||||
}
|
||||
:root[data-veilix-theme="dark"]{
|
||||
--bg: oklch(23% 0.026 210);
|
||||
--surface: oklch(26.5% 0.028 210);
|
||||
--surface-2: oklch(30% 0.03 210);
|
||||
--ink: oklch(96% 0.012 196);
|
||||
--ink-2: oklch(80% 0.02 196);
|
||||
--ink-3: oklch(65% 0.025 196);
|
||||
--border: oklch(36% 0.04 204);
|
||||
--accent: oklch(76% 0.105 190);
|
||||
--accent-hover: oklch(83% 0.09 190);
|
||||
--accent-soft: oklch(31% 0.05 190);
|
||||
--sel: oklch(30% 0.03 195);
|
||||
--on-accent: oklch(20% 0.04 204);
|
||||
--glow-1: oklch(52% 0.09 200);
|
||||
--glow-2: oklch(47% 0.085 215);
|
||||
--glow-3: oklch(50% 0.08 180);
|
||||
--veil: oklch(100% 0 0 / 0.07);
|
||||
--land: oklch(31% 0.035 204);
|
||||
--land-line: oklch(37% 0.04 204);
|
||||
--shadow: 0 1px 2px oklch(12% 0.03 204 / 0.4), 0 16px 40px -16px oklch(9% 0.03 204 / 0.7);
|
||||
}
|
||||
*{ box-sizing: border-box; }
|
||||
input, textarea, select{ font-size: 16px; }
|
||||
[hidden]{ display: none !important; }
|
||||
body{ margin:0; background: var(--bg); color: var(--ink); font-family:"Commissioner", system-ui, sans-serif; -webkit-font-smoothing: antialiased; }
|
||||
a{ color: var(--accent); text-decoration: none; }
|
||||
a:hover{ color: var(--accent-hover); }
|
||||
@keyframes veilPulse{
|
||||
0%{ transform: scale(1); opacity: 0.55; }
|
||||
70%{ transform: scale(2.6); opacity: 0; }
|
||||
100%{ transform: scale(2.6); opacity: 0; }
|
||||
}
|
||||
</style>
|
||||
</helmet>
|
||||
|
||||
<template id="__bundler_thumbnail" data-bg-color="#e3f4f1">
|
||||
<svg viewBox="0 0 1200 800" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="480" y="290" width="150" height="150" rx="48" fill="#00705e" />
|
||||
<rect x="570" y="360" width="150" height="150" rx="48" fill="#ffffff" fill-opacity="0.62" stroke="#00705e" stroke-width="6" />
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<div style="background: var(--bg); color: var(--ink); font-family: 'Commissioner', system-ui, sans-serif; font-weight: 400; letter-spacing: -0.01em; min-height: 100vh;">
|
||||
|
||||
<header style="position: sticky; top: 0; z-index: 40; background: color-mix(in oklab, var(--bg) 82%, transparent); backdrop-filter: blur(20px) saturate(1.4); -webkit-backdrop-filter: blur(20px) saturate(1.4); border-bottom: 1px solid var(--border);">
|
||||
<div style="max-width: 1200px; margin: 0 auto; padding: 14px 24px; min-height: 66px; display: flex; flex-wrap: nowrap; align-items: center; gap: 14px 40px;">
|
||||
<a href="MassMain.dc.html" style="display: flex; align-items: center; gap: 10px; color: var(--ink); flex: none;">
|
||||
<span style="position: relative; width: 28px; height: 28px; display: block; flex: none;">
|
||||
<span style="position: absolute; left: 0; top: 3px; width: 21px; height: 21px; border-radius: 7px; background: linear-gradient(140deg, var(--accent), color-mix(in oklab, var(--accent) 55%, var(--glow-3))); display: block;"></span>
|
||||
<span style="position: absolute; right: 0; bottom: 3px; width: 21px; height: 21px; border-radius: 7px; background: var(--veil); border: 1px solid color-mix(in oklab, var(--accent) 35%, transparent); backdrop-filter: blur(2px); display: block;"></span>
|
||||
</span>
|
||||
<span style="font-family: 'Unbounded', 'Commissioner', sans-serif; font-weight: 600; font-size: 17px; letter-spacing: -0.02em; width: 132px; flex: none; white-space: nowrap;">ВейликсВПН</span>
|
||||
</a>
|
||||
<nav hidden="{{ navOff }}" style="display: flex; flex-wrap: wrap; gap: 10px 28px; font-size: 15px; font-weight: 500; color: var(--ink-2);">
|
||||
<a href="MassPrice.dc.html" style="color: var(--ink-2);" style-hover="color: var(--ink);">Цены</a>
|
||||
<a href="#top" style="color: var(--ink);">Локации</a>
|
||||
<a href="MassSupport.dc.html" style="color: var(--ink-2);" style-hover="color: var(--ink);">Поддержка</a>
|
||||
</nav>
|
||||
<div style="margin-left: auto; display: flex; flex-wrap: wrap; align-items: center; gap: 8px;">
|
||||
<div hidden="{{ navOff }}" style="position: relative;">
|
||||
<button type="button" onClick="{{ toggleLangMenu }}" title="Язык" style="display: flex; align-items: center; gap: 7px; height: 38px; padding: 0 11px; border: 1px solid var(--border); border-radius: 10px; background: transparent; color: var(--ink-2); font-family: inherit; font-size: 14px; cursor: pointer;" style-hover="color: var(--ink); border-color: var(--ink-3);">
|
||||
<span style="position: relative; width: 16px; height: 16px; border-radius: 50%; border: 1.5px solid currentColor; display: block; flex: none;">
|
||||
<span style="position: absolute; left: -1.5px; top: 5.75px; width: 16px; height: 1.5px; background: currentColor; display: block;"></span>
|
||||
<span style="position: absolute; left: 3.25px; top: -1.5px; width: 8px; height: 16px; border-radius: 50%; border: 1.5px solid currentColor; display: block;"></span>
|
||||
</span>
|
||||
<span style="font-variant-numeric: tabular-nums;">{{ langLabel }}</span>
|
||||
</button>
|
||||
<div hidden="{{ langMenuOff }}" style="position: absolute; right: 0; top: 46px; width: 172px; padding: 6px; background: var(--surface); border: 1px solid var(--border); border-radius: 14px; box-shadow: var(--shadow); font-size: 14px;">
|
||||
<button type="button" onClick="{{ setRu }}" style="display: flex; align-items: center; justify-content: space-between; gap: 12px; width: 100%; padding: 9px 10px; border: 0; border-radius: 9px; background: {{ ruBg }}; color: var(--ink); font-family: inherit; font-size: 14px; text-align: left; cursor: pointer;">
|
||||
<span>Русский</span>
|
||||
<span style="color: var(--accent);">{{ ruCheck }}</span>
|
||||
</button>
|
||||
<button type="button" onClick="{{ setEn }}" style="display: flex; align-items: center; justify-content: space-between; gap: 12px; width: 100%; padding: 9px 10px; border: 0; border-radius: 9px; background: {{ enBg }}; color: var(--ink); font-family: inherit; font-size: 14px; text-align: left; cursor: pointer;">
|
||||
<span>English</span>
|
||||
<span style="color: var(--accent);">{{ enCheck }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<a href="MassApp.dc.html" hidden="{{ navOff }}" style="display: flex; align-items: center; justify-content: center; height: 38px; width: 78px; flex: none; box-sizing: border-box; border-radius: 10px; color: var(--ink-2); font-size: 15px;" style-hover="color: var(--ink); background: var(--surface-2);">Войти</a>
|
||||
<a href="MassOrder.dc.html" hidden="{{ navOff }}" style="display: flex; align-items: center; justify-content: center; height: 38px; width: 142px; flex: none; box-sizing: border-box; border-radius: 10px; background: var(--accent); color: var(--on-accent); font-size: 15px; font-weight: 500; box-shadow: 0 6px 18px -8px var(--accent);" style-hover="background: var(--accent-hover); color: var(--on-accent);">Получить VPN</a>
|
||||
<button type="button" hidden="{{ burgerOff }}" onClick="{{ toggleMenu }}" title="Меню" style="display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 5px; width: 42px; height: 38px; border: 1px solid var(--border); border-radius: 10px; background: transparent; color: var(--ink); cursor: pointer;" style-hover="border-color: var(--ink-3);">
|
||||
<span style="width: 18px; height: 2px; border-radius: 2px; background: currentColor; display: block;"></span>
|
||||
<span style="width: 18px; height: 2px; border-radius: 2px; background: currentColor; display: block;"></span>
|
||||
<span style="width: 18px; height: 2px; border-radius: 2px; background: currentColor; display: block;"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div hidden="{{ menuOff }}" style="position: absolute; left: 0; right: 0; top: 100%; z-index: 45; padding: 10px 20px 18px; background: var(--bg); border-bottom: 1px solid var(--border); box-shadow: var(--shadow); display: grid; gap: 2px;">
|
||||
<a href="MassPrice.dc.html" onClick="{{ closeMenu }}" style="padding: 13px 6px; font-size: 17px; color: var(--ink);">Цены</a>
|
||||
<a href="#top" onClick="{{ closeMenu }}" style="padding: 13px 6px; font-size: 17px; color: var(--ink);">Локации</a>
|
||||
<a href="MassSupport.dc.html" onClick="{{ closeMenu }}" style="padding: 13px 6px; font-size: 17px; color: var(--ink);">Поддержка</a>
|
||||
<div style="display: flex; align-items: center; justify-content: space-between; gap: 12px; padding-top: 6px; margin-top: 6px; border-top: 1px solid var(--border);">
|
||||
<a href="MassApp.dc.html" onClick="{{ closeMenu }}" style="padding: 13px 6px; font-size: 17px; color: var(--ink);">Войти</a>
|
||||
<div style="display: flex; gap: 6px; flex: none;">
|
||||
<button type="button" onClick="{{ setRu }}" style="padding: 9px 13px; border: 1px solid var(--border); border-radius: 10px; background: {{ ruBg }}; color: var(--ink); font-family: inherit; font-size: 13px; cursor: pointer; white-space: nowrap;">Русский</button>
|
||||
<button type="button" onClick="{{ setEn }}" style="padding: 9px 13px; border: 1px solid var(--border); border-radius: 10px; background: {{ enBg }}; color: var(--ink); font-family: inherit; font-size: 13px; cursor: pointer; white-space: nowrap;">English</button>
|
||||
</div>
|
||||
</div>
|
||||
<a href="MassOrder.dc.html" onClick="{{ closeMenu }}" style="display: flex; align-items: center; justify-content: center; height: 50px; margin-top: 12px; border-radius: 12px; background: var(--accent); color: var(--on-accent); font-size: 16px; font-weight: 500;">Получить VPN</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main id="top" style="max-width: 1200px; margin: 0 auto; padding: 0 clamp(14px, 4vw, 24px) 80px;">
|
||||
|
||||
<section style="position: relative; isolation: isolate; margin-top: clamp(14px, 3vw, 40px); border-radius: 28px; border: 1px solid var(--border); background: var(--surface); box-shadow: var(--shadow);">
|
||||
<div style="position: absolute; inset: 0; border-radius: 28px; overflow: hidden; pointer-events: none; -webkit-mask-image: -webkit-radial-gradient(white, black); mask-image: radial-gradient(white, black);">
|
||||
<div style="position: absolute; right: min(-90px, calc(-150px + 11vw)); top: min(-130px, calc(-190px + 11vw)); width: clamp(190px, 40vw, 440px); height: clamp(190px, 40vw, 440px); border-radius: 50%; background: radial-gradient(circle at 50% 50%, var(--glow-1) 0%, color-mix(in oklab, var(--glow-1) 35%, transparent) 55%, transparent 78%); filter: blur(80px); opacity: 0.45;"></div>
|
||||
<div style="position: absolute; left: min(-70px, calc(-130px + 11vw)); bottom: min(-150px, calc(-210px + 11vw)); width: clamp(160px, 34vw, 380px); height: clamp(160px, 34vw, 380px); border-radius: 50%; background: radial-gradient(circle at 50% 50%, var(--glow-3) 0%, color-mix(in oklab, var(--glow-3) 35%, transparent) 55%, transparent 78%); filter: blur(80px); opacity: 0.35;"></div>
|
||||
</div>
|
||||
<div style="position: relative; padding: clamp(22px, 5vw, 48px) clamp(14px, 3.5vw, 40px) clamp(20px, 4vw, 36px);">
|
||||
<h1 style="margin: 0 0 12px; font-size: clamp(34px, 4.4vw, 52px); font-weight: 600; letter-spacing: -0.035em;">Локации</h1>
|
||||
<p style="margin: 0 0 28px; max-width: 46em; font-size: 17px; line-height: 1.55; color: var(--ink-2); font-weight: 300;">30 стран и российский адрес входят в подписку.</p>
|
||||
<div id="veilix-map" style="width: 100%; min-height: 240px;"></div>
|
||||
<div style="margin-top: 10px; font-size: 13px; color: var(--ink-3);">Нажмите точку на карте — покажем страну и город</div>
|
||||
<div hidden="{{ swipeOff }}" style="margin-top: 4px; font-size: 13px; color: var(--ink-3);">Карту можно прокрутить в сторону</div>
|
||||
<div style="display: flex; flex-wrap: wrap; gap: 10px 24px; margin-top: 20px; font-size: 14px; color: var(--ink-2);">
|
||||
<span style="display: inline-flex; align-items: center; gap: 8px;">
|
||||
<span style="width: 9px; height: 9px; border-radius: 50%; background: var(--accent); display: block;"></span>
|
||||
Доступно в подписке
|
||||
</span>
|
||||
<span style="display: inline-flex; align-items: center; gap: 8px;">
|
||||
<span style="width: 9px; height: 9px; border-radius: 50%; border: 1.5px solid var(--ink-3); display: block;"></span>
|
||||
Скоро
|
||||
</span>
|
||||
<span style="display: inline-flex; align-items: center; gap: 8px;">
|
||||
<span style="width: 9px; height: 9px; border-radius: 2px; background: var(--ink-2); display: block;"></span>
|
||||
RU-адрес входит в подписку
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section style="padding: 56px 0 0;">
|
||||
<h2 style="margin: 0 0 10px; font-size: clamp(26px, 2.8vw, 34px); font-weight: 600; letter-spacing: -0.03em;">Все страны подписки</h2>
|
||||
<p style="margin: 0 0 24px; max-width: 52em; font-size: 15px; line-height: 1.55; color: var(--ink-2); font-weight: 300;">Одна цена за всё: 699 ₽ в месяц или 6 990 ₽ в год.</p>
|
||||
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 12px;">
|
||||
<sc-for list="{{ items }}" as="loc" hint-placeholder-count="12">
|
||||
<div role="button" tabindex="0" onClick="{{ loc.pick }}" style="display: grid; align-content: start; gap: 14px; padding: 22px; border-radius: 22px; border: 1px solid {{ loc.border }}; background: {{ loc.bg }}; color: var(--ink); cursor: pointer;" style-hover="border-color: var(--ink-3);">
|
||||
<span style="display: flex; align-items: center; justify-content: space-between; gap: 14px;">
|
||||
{{ loc.flag }}
|
||||
<span style="display: flex; align-items: center; gap: 8px; flex: none; min-height: 32px; font-size: 13px; color: {{ loc.statusInk }};">
|
||||
<span style="width: 6px; height: 6px; border-radius: 50%; background: {{ loc.statusDot }}; border: {{ loc.statusRing }}; display: block;"></span>
|
||||
{{ loc.badge }}
|
||||
</span>
|
||||
</span>
|
||||
<span style="display: grid; gap: 5px;">
|
||||
<span style="font-size: 19px; font-weight: 500; letter-spacing: -0.02em;">{{ loc.country }}</span>
|
||||
<span style="font-size: 14px; color: var(--ink-3);">{{ loc.city }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</sc-for>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section style="padding: 56px 0 0;">
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 16px;">
|
||||
<div style="padding: clamp(20px, 5vw, 32px); border-radius: 22px; background: var(--surface); border: 1px solid var(--border); box-shadow: var(--shadow);">
|
||||
<h3 style="margin: 0 0 10px; font-size: 22px; font-weight: 600; letter-spacing: -0.025em;">Российский адрес в подписке</h3>
|
||||
<p style="margin: 0; font-size: 15px; line-height: 1.55; color: var(--ink-2); font-weight: 300;">Он работает в обе стороны: из России банки и госуслуги видят обычное местное подключение, из другой страны те же сервисы открываются так, будто вы дома.</p>
|
||||
</div>
|
||||
<div style="padding: clamp(20px, 5vw, 32px); border-radius: 22px; background: var(--surface); border: 1px solid var(--border); box-shadow: var(--shadow);">
|
||||
<h3 id="lead" style="margin: 0 0 10px; font-size: 22px; font-weight: 600; letter-spacing: -0.025em; scroll-margin-top: 90px;">Нужны особые условия</h3>
|
||||
<p style="margin: 0 0 18px; font-size: 15px; line-height: 1.55; color: var(--ink-2); font-weight: 300;">Нужной страны нет в списке или нужна подписка на команду — напишите, ответим в течение дня.</p>
|
||||
<button type="button" onClick="{{ openLead }}" hidden="{{ leadOpen }}" style="display: inline-flex; align-items: center; height: 46px; padding: 0 20px; border-radius: 12px; border: 1px solid var(--accent); background: transparent; color: var(--accent); font-family: inherit; font-size: 15px; font-weight: 500; cursor: pointer;" style-hover="background: var(--accent-soft);">Оставить заявку</button>
|
||||
<div hidden="{{ leadClosed }}" style="display: grid; gap: 10px; max-width: 460px;">
|
||||
<input type="text" placeholder="Имя" style="height: 48px; padding: 0 14px; border-radius: 12px; border: 1px solid var(--border); background: var(--bg); color: var(--ink); font-family: inherit; font-size: 15px; width: 100%;" />
|
||||
<input type="email" placeholder="Email" style="height: 48px; padding: 0 14px; border-radius: 12px; border: 1px solid var(--border); background: var(--bg); color: var(--ink); font-family: inherit; font-size: 15px; width: 100%;" />
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 10px;">
|
||||
<input type="text" placeholder="Telegram" style="height: 48px; padding: 0 14px; border-radius: 12px; border: 1px solid var(--border); background: var(--bg); color: var(--ink); font-family: inherit; font-size: 15px; width: 100%;" />
|
||||
<input type="text" placeholder="WhatsApp" style="height: 48px; padding: 0 14px; border-radius: 12px; border: 1px solid var(--border); background: var(--bg); color: var(--ink); font-family: inherit; font-size: 15px; width: 100%;" />
|
||||
</div>
|
||||
<textarea rows="3" placeholder="Коротко о задаче" style="padding: 12px 14px; border-radius: 12px; border: 1px solid var(--border); background: var(--bg); color: var(--ink); font-family: inherit; font-size: 15px; resize: vertical; width: 100%;"></textarea>
|
||||
<button type="button" onClick="{{ sendLead }}" style="display: inline-flex; align-items: center; justify-content: center; height: 48px; padding: 0 22px; border-radius: 12px; border: 0; background: var(--accent); color: var(--on-accent); font-family: inherit; font-size: 15px; font-weight: 500; cursor: pointer;" style-hover="background: var(--accent-hover);">{{ leadButton }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer style="border-top: 1px solid var(--border); background: var(--surface-2);">
|
||||
<div style="max-width: 1200px; margin: 0 auto; padding: 48px clamp(16px, 4vw, 24px) 32px; display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 32px;">
|
||||
<div>
|
||||
<div style="display: flex; align-items: center; gap: 10px; margin-bottom: 14px;">
|
||||
<span style="position: relative; width: 24px; height: 24px; display: block; flex: none;">
|
||||
<span style="position: absolute; left: 0; top: 3px; width: 18px; height: 18px; border-radius: 6px; background: linear-gradient(140deg, var(--accent), color-mix(in oklab, var(--accent) 55%, var(--glow-3))); display: block;"></span>
|
||||
<span style="position: absolute; right: 0; bottom: 3px; width: 18px; height: 18px; border-radius: 6px; background: var(--veil); border: 1px solid color-mix(in oklab, var(--accent) 35%, transparent); display: block;"></span>
|
||||
</span>
|
||||
<span style="font-family: 'Unbounded', 'Commissioner', sans-serif; font-weight: 600; font-size: 15px; letter-spacing: -0.02em;">ВейликсВПН</span>
|
||||
</div>
|
||||
<p style="margin: 0 0 20px; max-width: 26em; font-size: 14px; line-height: 1.55; color: var(--ink-3); font-weight: 300;">VPN, который не нужно отключать. Работаем с 2022 года.</p>
|
||||
<div style="display: flex; flex-wrap: wrap; gap: 6px;">
|
||||
<span style="display: flex; align-items: center; justify-content: center; width: 60px; height: 28px; border-radius: 8px; border: 1px solid var(--border); background: var(--surface); font-size: 11px; letter-spacing: 0.06em; color: var(--ink-3);">VISA</span>
|
||||
<span style="display: flex; align-items: center; justify-content: center; width: 60px; height: 28px; border-radius: 8px; border: 1px solid var(--border); background: var(--surface); font-size: 11px; letter-spacing: 0.06em; color: var(--ink-3);">MC</span>
|
||||
<span style="display: flex; align-items: center; justify-content: center; width: 60px; height: 28px; border-radius: 8px; border: 1px solid var(--border); background: var(--surface); font-size: 11px; letter-spacing: 0.06em; color: var(--ink-3);">МИР</span>
|
||||
<span style="display: flex; align-items: center; justify-content: center; width: 60px; height: 28px; border-radius: 8px; border: 1px solid var(--border); background: var(--surface); font-size: 11px; letter-spacing: 0.06em; color: var(--ink-3);">CRYPTO</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: grid; grid-template-columns: repeat(2, minmax(130px, 1fr)); gap: 24px 32px; align-content: start;">
|
||||
<div style="display: grid; grid-template-columns: minmax(0, 1fr); gap: 10px; align-content: start; font-size: 14px;">
|
||||
<div style="font-size: 13px; color: var(--ink-3); margin-bottom: 2px;">Сервис</div>
|
||||
<a href="MassPrice.dc.html" style="color: var(--ink-2);" style-hover="color: var(--ink);">Цены</a>
|
||||
<a href="#top" style="color: var(--ink-2);" style-hover="color: var(--ink);">Локации</a>
|
||||
<a href="MassSupport.dc.html" style="color: var(--ink-2);" style-hover="color: var(--ink);">Поддержка</a>
|
||||
</div>
|
||||
<div style="display: grid; grid-template-columns: minmax(0, 1fr); gap: 10px; align-content: start; font-size: 14px;">
|
||||
<div style="font-size: 13px; color: var(--ink-3); margin-bottom: 2px;">Документы</div>
|
||||
<a href="#top" style="color: var(--ink-2);" style-hover="color: var(--ink);">Условия использования</a>
|
||||
<a href="#top" style="color: var(--ink-2);" style-hover="color: var(--ink);">Политика конфиденциальности</a>
|
||||
<a href="#top" style="color: var(--ink-2);" style-hover="color: var(--ink);">Возврат средств</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="max-width: 1200px; margin: 0 auto; padding: 0 clamp(16px, 4vw, 24px) 40px; display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 14px;">
|
||||
<div style="font-size: 13px; color: var(--ink-3);">© 2026 ВейликсВПН</div>
|
||||
<div style="display: flex; align-items: center; gap: 8px;">
|
||||
<div style="display: flex; padding: 2px; border-radius: 9px; border: 1px solid var(--border); background: var(--surface); flex: none;">
|
||||
<button type="button" onClick="{{ setLight }}" title="Светлая тема" style="display: flex; align-items: center; justify-content: center; width: 28px; height: 28px; border: 0; border-radius: 7px; background: {{ lightBg }}; color: {{ lightInk }}; cursor: pointer;"><span style="position: relative; width: 14px; height: 14px; display: block;"><span style="position: absolute; left: 3px; top: 3px; width: 8px; height: 8px; border-radius: 50%; background: currentColor; display: block;"></span><span style="position: absolute; left: 0; top: 0; width: 14px; height: 14px; border-radius: 50%; border: 1.5px solid currentColor; opacity: 0.45; display: block;"></span></span></button>
|
||||
<button type="button" onClick="{{ setDark }}" title="Тёмная тема" style="display: flex; align-items: center; justify-content: center; width: 28px; height: 28px; border: 0; border-radius: 7px; background: {{ darkBg }}; color: {{ darkInk }}; cursor: pointer;"><span style="width: 14px; height: 14px; border-radius: 50%; box-shadow: inset -5px -1.5px 0 0 currentColor; display: block;"></span></button>
|
||||
<button type="button" onClick="{{ setSystem }}" title="Как в системе" style="display: flex; align-items: center; justify-content: center; width: 28px; height: 28px; border: 0; border-radius: 7px; background: {{ systemBg }}; color: {{ systemInk }}; cursor: pointer;"><span style="width: 14px; height: 14px; border-radius: 50%; border: 1.5px solid currentColor; background: linear-gradient(90deg, currentColor 50%, transparent 50%); display: block;"></span></button>
|
||||
</div>
|
||||
<div style="display: flex; padding: 2px; border-radius: 9px; border: 1px solid var(--border); background: var(--surface); flex: none;">
|
||||
<button type="button" onClick="{{ setRu }}" style="padding: 0 10px; height: 28px; border: 0; border-radius: 7px; background: {{ ruBg }}; color: {{ ruInk }}; font-family: inherit; font-size: 12px; cursor: pointer;">RU</button>
|
||||
<button type="button" onClick="{{ setEn }}" style="padding: 0 10px; height: 28px; border: 0; border-radius: 7px; background: {{ enBg }}; color: {{ enInk }}; font-family: inherit; font-size: 12px; cursor: pointer;">EN</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</x-dc>
|
||||
<script type="text/x-dc" data-dc-script data-props="{"defaultTheme":{"editor":"enum","options":["light","dark","system"],"default":"light","tsType":"'light' | 'dark' | 'system'","section":"Тема"}}">
|
||||
class Component extends DCLogic {
|
||||
state = { theme: null, sysDark: false, lang: "ru", langMenu: false, narrow: false, menu: false, loc: 0, lead: false, leadSent: false };
|
||||
|
||||
data() {
|
||||
const base = { setup: "4 900 ₽", month: "2 499 ₽" };
|
||||
const rare = { setup: "6 900 ₽", month: "3 499 ₽" };
|
||||
const far = { setup: "9 900 ₽", month: "4 999 ₽" };
|
||||
const L = (country, city, code, lng, lat, ready, tier) => ({ country, city, code, lng, lat, ready, setup: tier.setup, month: tier.month, rare: tier !== base });
|
||||
return [
|
||||
L("Нидерланды", "Амстердам", "nl", 4.9, 52.37, true, base),
|
||||
L("Германия", "Франкфурт", "de", 8.68, 50.11, true, base),
|
||||
L("США", "Нью-Йорк", "us", -74.01, 40.71, true, base),
|
||||
L("Великобритания", "Лондон", "gb", -0.13, 51.51, true, base),
|
||||
L("Франция", "Париж", "fr", 2.35, 48.86, true, base),
|
||||
L("Финляндия", "Хельсинки", "fi", 24.94, 60.17, true, base),
|
||||
L("Швеция", "Стокгольм", "se", 18.07, 59.33, true, base),
|
||||
L("Норвегия", "Осло", "no", 10.75, 59.91, false, base),
|
||||
L("Дания", "Копенгаген", "dk", 12.57, 55.68, false, base),
|
||||
L("Швейцария", "Цюрих", "ch", 8.54, 47.37, true, rare),
|
||||
L("Австрия", "Вена", "at", 16.37, 48.21, true, base),
|
||||
L("Испания", "Мадрид", "es", -3.7, 40.42, true, base),
|
||||
L("Италия", "Милан", "it", 9.19, 45.46, true, base),
|
||||
L("Польша", "Варшава", "pl", 21.01, 52.23, true, base),
|
||||
L("Чехия", "Прага", "cz", 14.44, 50.08, true, base),
|
||||
L("Румыния", "Бухарест", "ro", 26.1, 44.43, false, base),
|
||||
L("Литва", "Вильнюс", "lt", 25.28, 54.69, true, base),
|
||||
L("Латвия", "Рига", "lv", 24.11, 56.95, true, base),
|
||||
L("Эстония", "Таллин", "ee", 24.75, 59.44, false, base),
|
||||
L("Турция", "Стамбул", "tr", 28.98, 41.01, true, base),
|
||||
L("Казахстан", "Алматы", "kz", 76.89, 43.24, false, base),
|
||||
L("Армения", "Ереван", "am", 44.51, 40.18, false, base),
|
||||
L("Грузия", "Тбилиси", "ge", 44.8, 41.72, false, base),
|
||||
L("ОАЭ", "Дубай", "ae", 55.27, 25.2, true, rare),
|
||||
L("Сингапур", "Сингапур", "sg", 103.82, 1.35, true, rare),
|
||||
L("Япония", "Токио", "jp", 139.69, 35.69, true, rare),
|
||||
L("Южная Корея", "Сеул", "kr", 126.98, 37.57, false, rare),
|
||||
L("Канада", "Торонто", "ca", -79.38, 43.65, true, base),
|
||||
L("Австралия", "Сидней", "au", 151.21, -33.87, false, far),
|
||||
L("Бразилия", "Сан-Паулу", "br", -46.63, -23.55, false, far)
|
||||
];
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const mq = window.matchMedia("(prefers-color-scheme: dark)");
|
||||
this.mq = mq;
|
||||
this.onSys = () => this.setState({ sysDark: mq.matches });
|
||||
mq.addEventListener("change", this.onSys);
|
||||
this.onResize = () => {
|
||||
const n = window.innerWidth < 900;
|
||||
if (n !== this.state.narrow) this.setState({ narrow: n, menu: false });
|
||||
};
|
||||
window.addEventListener("resize", this.onResize);
|
||||
this.onResize();
|
||||
let saved = null;
|
||||
try { saved = localStorage.getItem("veilix.theme"); } catch (e) {}
|
||||
let savedLang = null;
|
||||
try { savedLang = localStorage.getItem("veilix.lang"); } catch (e) {}
|
||||
this.setState({ sysDark: mq.matches, theme: saved || this.props.defaultTheme || "light", lang: savedLang || "ru" }, () => {
|
||||
this.apply();
|
||||
this.applyLang();
|
||||
});
|
||||
this.drawMap();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.mq) this.mq.removeEventListener("change", this.onSys);
|
||||
if (this.onResize) window.removeEventListener("resize", this.onResize);
|
||||
}
|
||||
|
||||
componentDidUpdate() { this.apply(); this.applyLang(); }
|
||||
|
||||
apply() {
|
||||
const theme = this.state.theme || this.props.defaultTheme || "light";
|
||||
const dark = theme === "dark" || (theme === "system" && this.state.sysDark);
|
||||
document.documentElement.setAttribute("data-veilix-theme", dark ? "dark" : "light");
|
||||
}
|
||||
|
||||
applyLang() {
|
||||
const lang = this.state.lang || "ru";
|
||||
if (window.VEILIX_APPLY_LANG) window.VEILIX_APPLY_LANG(document.body, lang);
|
||||
else setTimeout(() => this.applyLang(), 150);
|
||||
}
|
||||
|
||||
setTheme(t) {
|
||||
try { localStorage.setItem("veilix.theme", t); } catch (e) {}
|
||||
this.setState({ theme: t }, () => this.apply());
|
||||
}
|
||||
|
||||
setLang(l) {
|
||||
try { localStorage.setItem("veilix.lang", l); } catch (e) {}
|
||||
this.setState({ lang: l, langMenu: false }, () => this.applyLang());
|
||||
}
|
||||
|
||||
async drawMap() {
|
||||
const host = document.getElementById("veilix-map");
|
||||
if (!host) return;
|
||||
if (!window.d3 || !window.topojson) { setTimeout(() => this.drawMap(), 200); return; }
|
||||
const d3 = window.d3;
|
||||
let topo;
|
||||
try {
|
||||
topo = await d3.json("https://cdn.jsdelivr.net/npm/world-atlas@2.0.2/countries-110m.json");
|
||||
} catch (e) { return; }
|
||||
const land = window.topojson.feature(topo, topo.objects.countries);
|
||||
const W = 1200, H = 520;
|
||||
const projection = d3.geoNaturalEarth1().fitExtent([[10, 10], [W - 10, H - 10]], land);
|
||||
const path = d3.geoPath(projection);
|
||||
|
||||
host.innerHTML = "";
|
||||
const center = () => {
|
||||
const extra = host.scrollWidth - host.clientWidth;
|
||||
if (extra > 0) host.scrollLeft = extra / 2;
|
||||
};
|
||||
this.centerMap = center;
|
||||
if (!this.mapCentering) {
|
||||
this.mapCentering = true;
|
||||
window.addEventListener("resize", () => this.centerMap && this.centerMap());
|
||||
}
|
||||
host.style.overflowX = "auto";
|
||||
host.style.overflowY = "hidden";
|
||||
host.style.borderRadius = "18px";
|
||||
const svg = d3.select(host).append("svg")
|
||||
.attr("viewBox", "0 0 " + W + " " + H)
|
||||
.attr("width", "100%")
|
||||
.style("display", "block")
|
||||
.style("min-width", "820px")
|
||||
.style("overflow", "visible");
|
||||
|
||||
svg.append("g").selectAll("path").data(land.features).join("path")
|
||||
.attr("d", path)
|
||||
.attr("fill", "var(--land)")
|
||||
.attr("stroke", "var(--land-line)")
|
||||
.attr("stroke-width", 0.8);
|
||||
|
||||
const ru = projection([37.62, 55.75]);
|
||||
if (ru) {
|
||||
svg.append("rect")
|
||||
.attr("x", ru[0] - 5).attr("y", ru[1] - 5)
|
||||
.attr("width", 10).attr("height", 10).attr("rx", 2.5)
|
||||
.attr("fill", "var(--ink-2)");
|
||||
svg.append("text")
|
||||
.attr("x", ru[0] + 12).attr("y", ru[1] + 4)
|
||||
.attr("fill", "var(--ink-2)")
|
||||
.attr("font-size", 15)
|
||||
.attr("font-family", "Commissioner, system-ui, sans-serif")
|
||||
.text("RU");
|
||||
}
|
||||
|
||||
const FONT = "Commissioner, system-ui, sans-serif";
|
||||
const closePopup = () => { if (this.popupG) { this.popupG.remove(); this.popupG = null; } };
|
||||
this.closePopup = closePopup;
|
||||
svg.on("click", closePopup);
|
||||
|
||||
const popup = (d, p) => {
|
||||
closePopup();
|
||||
const pw = 268, ph = 106;
|
||||
const x = Math.max(8, Math.min(W - pw - 8, p[0] - pw / 2));
|
||||
let y = p[1] - ph - 16;
|
||||
if (y < 8) y = Math.min(H - ph - 8, p[1] + 18);
|
||||
const g = svg.append("g").attr("transform", "translate(" + x + "," + y + ")");
|
||||
this.popupG = g;
|
||||
g.on("click", (ev) => ev.stopPropagation());
|
||||
g.append("rect").attr("width", pw).attr("height", ph).attr("rx", 16)
|
||||
.attr("fill", "var(--surface)").attr("stroke", "var(--border)");
|
||||
g.append("text").attr("x", 20).attr("y", 33).attr("fill", "var(--ink)")
|
||||
.attr("font-size", 17).attr("font-weight", 500).attr("font-family", FONT).text(d.country);
|
||||
g.append("text").attr("x", 20).attr("y", 55).attr("fill", "var(--ink-3)")
|
||||
.attr("font-size", 13).attr("font-family", FONT).text(d.city);
|
||||
const statusG = g.append("g").attr("transform", "translate(20, 78)");
|
||||
statusG.append("circle").attr("cx", 5).attr("cy", -4).attr("r", 4)
|
||||
.attr("fill", d.ready ? "var(--accent)" : "var(--bg)")
|
||||
.attr("stroke", d.ready ? "none" : "var(--ink-3)").attr("stroke-width", d.ready ? 0 : 1.5);
|
||||
statusG.append("text").attr("x", 17).attr("font-size", 13).attr("font-family", FONT)
|
||||
.attr("fill", d.ready ? "var(--accent)" : "var(--ink-3)")
|
||||
.text(d.ready ? "Доступно в подписке" : "Скоро");
|
||||
const close = g.append("g").style("cursor", "pointer").on("click", (ev) => { ev.stopPropagation(); closePopup(); });
|
||||
close.append("rect").attr("x", pw - 44).attr("y", 14).attr("width", 30).attr("height", 30).attr("rx", 9).attr("fill", "transparent");
|
||||
close.append("text").attr("x", pw - 29).attr("y", 35).attr("text-anchor", "middle")
|
||||
.attr("fill", "var(--ink-3)").attr("font-size", 18).attr("font-family", FONT).text("×");
|
||||
this.applyLang();
|
||||
};
|
||||
|
||||
this.data().forEach((d, i) => {
|
||||
const p = projection([d.lng, d.lat]);
|
||||
if (!p) return;
|
||||
const g = svg.append("g").attr("transform", "translate(" + p[0] + "," + p[1] + ")").style("cursor", "pointer");
|
||||
g.append("circle").attr("r", 12).attr("fill", "transparent");
|
||||
if (d.ready) {
|
||||
g.append("circle").attr("r", 6).attr("fill", "var(--accent)").attr("opacity", 0.32)
|
||||
.style("transform-box", "fill-box")
|
||||
.style("transform-origin", "center")
|
||||
.style("animation", "veilPulse 3.2s ease-out infinite");
|
||||
g.append("circle").attr("r", 5).attr("fill", "var(--accent)")
|
||||
.attr("stroke", "var(--surface)").attr("stroke-width", 1.4);
|
||||
} else {
|
||||
g.append("circle").attr("r", 5).attr("fill", "var(--bg)")
|
||||
.attr("stroke", "var(--ink-3)").attr("stroke-width", 2);
|
||||
}
|
||||
g.append("title").text(d.country + " · " + d.city);
|
||||
g.on("click", (ev) => { ev.stopPropagation(); this.setState({ loc: i }); popup(d, p); });
|
||||
});
|
||||
requestAnimationFrame(center);
|
||||
setTimeout(center, 120);
|
||||
}
|
||||
|
||||
flagSpecs() {
|
||||
return {
|
||||
nl: { rows: ["#AE1C28", "#FFFFFF", "#21468B"] },
|
||||
de: { rows: ["#000000", "#DD0000", "#FFCE00"] },
|
||||
us: { rows: ["#B22234", "#FFFFFF", "#B22234", "#FFFFFF", "#B22234", "#FFFFFF"], marks: [{ l: 0, t: 0, w: 22, h: 17, bg: "#3C3B6E" }] },
|
||||
gb: { bg: "#012169", marks: [{ l: 17, t: 0, w: 12, h: 32, bg: "#FFFFFF" }, { l: 0, t: 10, w: 46, h: 12, bg: "#FFFFFF" }, { l: 20, t: 0, w: 6, h: 32, bg: "#C8102E" }, { l: 0, t: 13, w: 46, h: 6, bg: "#C8102E" }] },
|
||||
fr: { cols: ["#002395", "#FFFFFF", "#ED2939"] },
|
||||
fi: { bg: "#FFFFFF", marks: [{ l: 13, t: 0, w: 8, h: 32, bg: "#003580" }, { l: 0, t: 12, w: 46, h: 8, bg: "#003580" }] },
|
||||
se: { bg: "#006AA7", marks: [{ l: 13, t: 0, w: 6, h: 32, bg: "#FECC00" }, { l: 0, t: 13, w: 46, h: 6, bg: "#FECC00" }] },
|
||||
no: { bg: "#BA0C2F", marks: [{ l: 12, t: 0, w: 10, h: 32, bg: "#FFFFFF" }, { l: 0, t: 11, w: 46, h: 10, bg: "#FFFFFF" }, { l: 15, t: 0, w: 4, h: 32, bg: "#00205B" }, { l: 0, t: 14, w: 46, h: 4, bg: "#00205B" }] },
|
||||
dk: { bg: "#C8102E", marks: [{ l: 13, t: 0, w: 6, h: 32, bg: "#FFFFFF" }, { l: 0, t: 13, w: 46, h: 6, bg: "#FFFFFF" }] },
|
||||
ch: { bg: "#D52B1E", marks: [{ l: 20, t: 8, w: 6, h: 16, bg: "#FFFFFF" }, { l: 15, t: 13, w: 16, h: 6, bg: "#FFFFFF" }] },
|
||||
at: { rows: ["#ED2939", "#FFFFFF", "#ED2939"] },
|
||||
es: { rows: ["#AA151B", "#F1BF00", "#AA151B"], sizes: "1fr 2fr 1fr" },
|
||||
it: { cols: ["#008C45", "#F4F5F0", "#CD212A"] },
|
||||
pl: { rows: ["#FFFFFF", "#DC143C"] },
|
||||
cz: { rows: ["#FFFFFF", "#D7141A"], marks: [{ l: 0, t: 0, w: 18, h: 32, bg: "#11457E", clip: "polygon(0 0, 100% 50%, 0 100%)" }] },
|
||||
ro: { cols: ["#002B7F", "#FCD116", "#CE1126"] },
|
||||
lt: { rows: ["#FDB913", "#006A44", "#C1272D"] },
|
||||
lv: { rows: ["#9E3039", "#FFFFFF", "#9E3039"], sizes: "2fr 1fr 2fr" },
|
||||
ee: { rows: ["#0072CE", "#000000", "#FFFFFF"] },
|
||||
tr: { bg: "#E30A17", marks: [{ l: 11, t: 8, w: 16, h: 16, r: "50%", bg: "#FFFFFF" }, { l: 16, t: 8, w: 16, h: 16, r: "50%", bg: "#E30A17" }] },
|
||||
kz: { bg: "#00AFCA", marks: [{ l: 16, t: 8, w: 15, h: 15, r: "50%", bg: "#FEC50C" }] },
|
||||
am: { rows: ["#D90012", "#0033A0", "#F2A800"] },
|
||||
ge: { bg: "#FFFFFF", marks: [{ l: 20, t: 0, w: 6, h: 32, bg: "#FF0000" }, { l: 0, t: 13, w: 46, h: 6, bg: "#FF0000" }] },
|
||||
ae: { rows: ["#00732F", "#FFFFFF", "#000000"], marks: [{ l: 0, t: 0, w: 11, h: 32, bg: "#FF0000" }] },
|
||||
sg: { rows: ["#EF3340", "#FFFFFF"], marks: [{ l: 5, t: 3, w: 11, h: 11, r: "50%", bg: "#FFFFFF" }, { l: 9, t: 3, w: 11, h: 11, r: "50%", bg: "#EF3340" }] },
|
||||
jp: { bg: "#FFFFFF", marks: [{ l: 16, t: 9, w: 14, h: 14, r: "50%", bg: "#BC002D" }] },
|
||||
kr: { bg: "#FFFFFF", marks: [{ l: 16, t: 9, w: 14, h: 14, r: "50%", bg: "#CD2E3A" }, { l: 16, t: 9, w: 14, h: 14, r: "50%", bg: "#0047A0", clip: "polygon(0 50%, 100% 50%, 100% 100%, 0 100%)" }] },
|
||||
ca: { cols: ["#D80621", "#FFFFFF", "#D80621"], sizes: "1fr 2fr 1fr", marks: [{ l: 20, t: 9, w: 7, h: 13, bg: "#D80621", clip: "polygon(50% 0, 100% 100%, 0 100%)" }] },
|
||||
au: { bg: "#00247D", marks: [{ l: 9, t: 0, w: 4, h: 16, bg: "#FFFFFF" }, { l: 0, t: 6, w: 22, h: 4, bg: "#FFFFFF" }, { l: 30, t: 19, w: 7, h: 7, r: "50%", bg: "#FFFFFF" }] },
|
||||
br: { bg: "#009739", marks: [{ l: 9, t: 4, w: 28, h: 24, bg: "#FEDD00", clip: "polygon(50% 0, 100% 50%, 50% 100%, 0 50%)" }, { l: 18, t: 11, w: 10, h: 10, r: "50%", bg: "#012169" }] }
|
||||
};
|
||||
}
|
||||
|
||||
flag(code) {
|
||||
const h = React.createElement;
|
||||
const spec = this.flagSpecs()[code] || {};
|
||||
const style = {
|
||||
width: "46px", height: "32px", borderRadius: "6px", overflow: "hidden",
|
||||
display: "grid", border: "1px solid var(--border)", position: "relative", flex: "none"
|
||||
};
|
||||
let bands = [];
|
||||
if (spec.rows) {
|
||||
bands = spec.rows;
|
||||
style.gridTemplateRows = spec.sizes || "repeat(" + bands.length + ", 1fr)";
|
||||
} else if (spec.cols) {
|
||||
bands = spec.cols;
|
||||
style.gridAutoFlow = "column";
|
||||
style.gridTemplateColumns = spec.sizes || "repeat(" + bands.length + ", 1fr)";
|
||||
} else {
|
||||
style.background = spec.bg || "var(--surface-2)";
|
||||
}
|
||||
const kids = bands.map((c, i) => h("span", { key: "b" + i, style: { background: c, display: "block" } }));
|
||||
(spec.marks || []).forEach((m, i) => kids.push(h("span", {
|
||||
key: "m" + i,
|
||||
style: {
|
||||
position: "absolute", left: m.l + "px", top: m.t + "px", width: m.w + "px", height: m.h + "px",
|
||||
borderRadius: m.r || 0, background: m.bg, clipPath: m.clip || "none", display: "block"
|
||||
}
|
||||
})));
|
||||
return h("span", { style: style }, kids);
|
||||
}
|
||||
|
||||
renderVals() {
|
||||
const s = this.state;
|
||||
const sel = "var(--sel)";
|
||||
const theme = s.theme || this.props.defaultTheme || "light";
|
||||
const rows = this.data();
|
||||
const cur = rows[s.loc];
|
||||
return {
|
||||
items: rows.map((d, i) => ({
|
||||
country: d.country,
|
||||
city: d.city,
|
||||
flag: this.flag(d.code),
|
||||
status: d.ready ? "Доступно в подписке" : "Скоро",
|
||||
badge: d.ready ? "Включено" : "Скоро",
|
||||
statusInk: d.ready ? "var(--accent)" : "var(--ink-3)",
|
||||
statusDot: d.ready ? "var(--accent)" : "transparent",
|
||||
statusRing: d.ready ? "0" : "1.5px solid var(--ink-3)",
|
||||
bg: s.loc === i ? sel : "var(--surface)",
|
||||
border: s.loc === i ? "var(--accent)" : "var(--border)",
|
||||
dot: s.loc === i ? "var(--accent)" : "transparent",
|
||||
dotOff: s.loc === i,
|
||||
buy: (e) => { e.stopPropagation(); window.location.href = "MassOrder.dc.html"; },
|
||||
setup: d.setup,
|
||||
month: d.month,
|
||||
buyOff: s.loc !== i,
|
||||
pick: () => this.setState({ loc: i })
|
||||
})),
|
||||
openLead: () => this.setState({ lead: true }),
|
||||
sendLead: () => this.setState({ leadSent: true }),
|
||||
leadOpen: this.state.lead,
|
||||
leadClosed: !this.state.lead,
|
||||
leadButton: this.state.leadSent ? "Заявка отправлена" : "Отправить",
|
||||
swipeOff: !s.narrow,
|
||||
navOff: s.narrow,
|
||||
burgerOff: !s.narrow,
|
||||
menuOff: !(s.narrow && s.menu),
|
||||
toggleMenu: () => this.setState(st => ({ menu: !st.menu, langMenu: false })),
|
||||
closeMenu: () => this.setState({ menu: false }),
|
||||
toggleLangMenu: () => this.setState(st => ({ langMenu: !st.langMenu })),
|
||||
langMenuOff: !s.langMenu,
|
||||
setRu: () => this.setLang("ru"),
|
||||
setEn: () => this.setLang("en"),
|
||||
langLabel: s.lang === "en" ? "EN" : "RU",
|
||||
ruBg: s.lang === "en" ? "transparent" : sel,
|
||||
enBg: s.lang === "en" ? sel : "transparent",
|
||||
ruInk: s.lang === "en" ? "var(--ink-3)" : "var(--ink)",
|
||||
enInk: s.lang === "en" ? "var(--ink)" : "var(--ink-3)",
|
||||
ruCheck: s.lang === "en" ? "" : "✓",
|
||||
enCheck: s.lang === "en" ? "✓" : "",
|
||||
setLight: () => this.setTheme("light"),
|
||||
setDark: () => this.setTheme("dark"),
|
||||
setSystem: () => this.setTheme("system"),
|
||||
lightBg: theme === "light" ? sel : "transparent",
|
||||
darkBg: theme === "dark" ? sel : "transparent",
|
||||
systemBg: theme === "system" ? sel : "transparent",
|
||||
lightInk: theme === "light" ? "var(--ink)" : "var(--ink-3)",
|
||||
darkInk: theme === "dark" ? "var(--ink)" : "var(--ink-3)",
|
||||
systemInk: theme === "system" ? "var(--ink)" : "var(--ink-3)"
|
||||
};
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,776 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<script src="./support.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<x-dc>
|
||||
<helmet>
|
||||
<link rel="icon" type="image/svg+xml" href="favicon-mass.svg" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin="anonymous" />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Commissioner:wght@300;400;500;600&family=Unbounded:wght@500;600&display=swap" rel="stylesheet" />
|
||||
<script src="https://unpkg.com/d3@7.9.0/dist/d3.min.js" crossorigin="anonymous"></script>
|
||||
<script src="https://unpkg.com/topojson-client@3.1.0/dist/topojson-client.min.js" crossorigin="anonymous"></script>
|
||||
<script>(function(){if(window.__veilixI18nSrc)return;window.__veilixI18nSrc="./i18n.js?v="+Date.now().toString(36)+Math.random().toString(36).slice(2,8);var s=document.createElement("script");s.src=window.__veilixI18nSrc;document.head.appendChild(s);})();</script>
|
||||
<style>
|
||||
:root{
|
||||
--bg: oklch(99.6% 0.0015 215);
|
||||
--surface: oklch(100% 0 0);
|
||||
--surface-2: oklch(98.4% 0.003 215);
|
||||
--ink: oklch(24% 0.04 204);
|
||||
--ink-2: oklch(45% 0.03 204);
|
||||
--ink-3: oklch(60% 0.025 204);
|
||||
--border: oklch(92% 0.014 200);
|
||||
--accent: oklch(48% 0.105 178);
|
||||
--accent-hover: oklch(41% 0.1 178);
|
||||
--accent-soft: oklch(95.5% 0.032 180);
|
||||
--sel: oklch(96.5% 0.016 190);
|
||||
--on-accent: oklch(99% 0.01 180);
|
||||
--glow-1: oklch(86% 0.085 190);
|
||||
--glow-2: oklch(88% 0.075 205);
|
||||
--glow-3: oklch(89% 0.07 172);
|
||||
--veil: oklch(100% 0 0 / 0.55);
|
||||
--land: oklch(95% 0.012 196);
|
||||
--land-line: oklch(90% 0.018 196);
|
||||
--ocean: oklch(97.5% 0.007 196);
|
||||
--shadow: 0 1px 2px oklch(24% 0.04 204 / 0.05), 0 12px 32px -12px oklch(24% 0.04 204 / 0.14);
|
||||
}
|
||||
:root[data-veilix-theme="dark"]{
|
||||
--bg: oklch(23% 0.026 210);
|
||||
--surface: oklch(26.5% 0.028 210);
|
||||
--surface-2: oklch(30% 0.03 210);
|
||||
--ink: oklch(96% 0.012 196);
|
||||
--ink-2: oklch(80% 0.02 196);
|
||||
--ink-3: oklch(65% 0.025 196);
|
||||
--border: oklch(36% 0.04 204);
|
||||
--accent: oklch(76% 0.105 190);
|
||||
--accent-hover: oklch(83% 0.09 190);
|
||||
--accent-soft: oklch(31% 0.05 190);
|
||||
--sel: oklch(30% 0.03 195);
|
||||
--on-accent: oklch(20% 0.04 204);
|
||||
--glow-1: oklch(52% 0.09 200);
|
||||
--glow-2: oklch(47% 0.085 215);
|
||||
--glow-3: oklch(50% 0.08 180);
|
||||
--veil: oklch(100% 0 0 / 0.07);
|
||||
--land: oklch(31% 0.035 204);
|
||||
--land-line: oklch(37% 0.04 204);
|
||||
--ocean: oklch(27% 0.035 204);
|
||||
--shadow: 0 1px 2px oklch(12% 0.03 204 / 0.4), 0 16px 40px -16px oklch(9% 0.03 204 / 0.7);
|
||||
}
|
||||
*{ box-sizing: border-box; }
|
||||
input, textarea, select{ font-size: 16px; }
|
||||
[hidden]{ display: none !important; }
|
||||
body{ margin:0; background: var(--bg); color: var(--ink); font-family:"Commissioner", system-ui, sans-serif; -webkit-font-smoothing: antialiased; }
|
||||
a{ color: var(--accent); text-decoration: none; }
|
||||
a:hover{ color: var(--accent-hover); }
|
||||
summary{ list-style: none; }
|
||||
@keyframes veilPulse{
|
||||
0%{ transform: scale(1); opacity: 0.32; }
|
||||
70%{ transform: scale(2.6); opacity: 0; }
|
||||
100%{ transform: scale(2.6); opacity: 0; }
|
||||
}
|
||||
@keyframes veilDrift{
|
||||
0%{ transform: translate3d(0,0,0) scale(1); }
|
||||
50%{ transform: translate3d(2%,-3%,0) scale(1.06); }
|
||||
100%{ transform: translate3d(0,0,0) scale(1); }
|
||||
}
|
||||
@keyframes veilDrift2{
|
||||
0%{ transform: translate3d(0,0,0) scale(1.04); }
|
||||
50%{ transform: translate3d(-3%,2%,0) scale(1); }
|
||||
100%{ transform: translate3d(0,0,0) scale(1.04); }
|
||||
}
|
||||
</style>
|
||||
</helmet>
|
||||
|
||||
<template id="__bundler_thumbnail" data-bg-color="#e3f4f1">
|
||||
<svg viewBox="0 0 1200 800" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="480" y="290" width="150" height="150" rx="48" fill="#00705e" />
|
||||
<rect x="570" y="360" width="150" height="150" rx="48" fill="#ffffff" fill-opacity="0.62" stroke="#00705e" stroke-width="6" />
|
||||
</svg>
|
||||
</template>
|
||||
<div style="background: var(--bg); color: var(--ink); font-family: 'Commissioner', system-ui, sans-serif; font-weight: 400; letter-spacing: -0.01em; min-height: 100vh; transition: background 240ms ease, color 240ms ease;">
|
||||
|
||||
<header style="position: sticky; top: 0; z-index: 40; background: color-mix(in oklab, var(--bg) 82%, transparent); backdrop-filter: blur(20px) saturate(1.4); -webkit-backdrop-filter: blur(20px) saturate(1.4); border-bottom: 1px solid var(--border);">
|
||||
<div style="max-width: 1200px; margin: 0 auto; padding: 14px 24px; min-height: 66px; display: flex; flex-wrap: nowrap; align-items: center; gap: 14px 40px;">
|
||||
|
||||
<a href="#top" style="display: flex; align-items: center; gap: 10px; color: var(--ink); flex: none;">
|
||||
<span style="position: relative; width: 28px; height: 28px; display: block; flex: none;">
|
||||
<span hidden="{{ mark1Off }}" style="position: absolute; inset: 0;">
|
||||
<span style="position: absolute; left: 0; top: 3px; width: 21px; height: 21px; border-radius: 7px; background: linear-gradient(140deg, var(--accent), color-mix(in oklab, var(--accent) 55%, var(--glow-3))); display: block;"></span>
|
||||
<span style="position: absolute; right: 0; bottom: 3px; width: 21px; height: 21px; border-radius: 7px; background: var(--veil); border: 1px solid color-mix(in oklab, var(--accent) 35%, transparent); backdrop-filter: blur(2px); display: block;"></span>
|
||||
</span>
|
||||
<span hidden="{{ mark2Off }}" style="position: absolute; inset: 0; overflow: hidden;">
|
||||
<span style="position: absolute; left: 2px; top: 2px; width: 24px; height: 24px; border-radius: 50%; background: linear-gradient(150deg, var(--accent), color-mix(in oklab, var(--accent) 50%, var(--glow-2))); display: block;"></span>
|
||||
<span style="position: absolute; left: -6px; top: 13px; width: 40px; height: 18px; border-radius: 10px 10px 4px 4px; background: color-mix(in oklab, var(--bg) 72%, transparent); backdrop-filter: blur(3px); transform: rotate(-14deg); display: block;"></span>
|
||||
</span>
|
||||
<span hidden="{{ mark3Off }}" style="position: absolute; inset: 0;">
|
||||
<span style="position: absolute; left: 1px; top: 1px; width: 26px; height: 26px; border-radius: 50%; border: 2px solid var(--accent); border-bottom-color: transparent; border-right-color: transparent; transform: rotate(-30deg); display: block;"></span>
|
||||
<span style="position: absolute; left: 7px; top: 7px; width: 14px; height: 14px; border-radius: 50%; border: 2px solid color-mix(in oklab, var(--accent) 45%, transparent); border-bottom-color: transparent; border-right-color: transparent; transform: rotate(20deg); display: block;"></span>
|
||||
</span>
|
||||
<span hidden="{{ mark4Off }}" style="position: absolute; inset: 0;">
|
||||
<span style="position: absolute; left: 4px; top: 4px; width: 20px; height: 20px; border-radius: 6px; background: linear-gradient(135deg, var(--accent), color-mix(in oklab, var(--accent) 45%, var(--glow-1))); transform: rotate(45deg); display: block;"></span>
|
||||
<span style="position: absolute; left: 9px; top: 9px; width: 10px; height: 10px; border-radius: 3px; background: color-mix(in oklab, var(--bg) 85%, transparent); transform: rotate(45deg); display: block;"></span>
|
||||
</span>
|
||||
</span>
|
||||
<span style="font-family: 'Unbounded', 'Commissioner', sans-serif; font-weight: 600; font-size: 17px; letter-spacing: -0.02em; width: 132px; flex: none; white-space: nowrap;">ВейликсВПН</span>
|
||||
</a>
|
||||
|
||||
<nav hidden="{{ navOff }}" style="display: flex; flex-wrap: wrap; gap: 10px 28px; font-size: 15px; font-weight: 500; color: var(--ink-2);">
|
||||
<a href="MassPrice.dc.html" style="color: var(--ink-2);" style-hover="color: var(--ink);">Цены</a>
|
||||
<a href="MassLocations.dc.html" style="color: var(--ink-2);" style-hover="color: var(--ink);">Локации</a>
|
||||
<a href="MassSupport.dc.html" style="color: var(--ink-2);" style-hover="color: var(--ink);">Поддержка</a>
|
||||
</nav>
|
||||
|
||||
<div style="margin-left: auto; display: flex; flex-wrap: wrap; align-items: center; gap: 8px;">
|
||||
<div hidden="{{ navOff }}" style="position: relative;">
|
||||
<button type="button" onClick="{{ toggleLangMenu }}" title="Язык" style="display: flex; align-items: center; gap: 7px; height: 38px; padding: 0 11px; border: 1px solid var(--border); border-radius: 10px; background: transparent; color: var(--ink-2); font-family: inherit; font-size: 14px; cursor: pointer;" style-hover="color: var(--ink); border-color: var(--ink-3);">
|
||||
<span style="position: relative; width: 16px; height: 16px; border-radius: 50%; border: 1.5px solid currentColor; display: block; flex: none;">
|
||||
<span style="position: absolute; left: -1.5px; top: 5.75px; width: 16px; height: 1.5px; background: currentColor; display: block;"></span>
|
||||
<span style="position: absolute; left: 3.25px; top: -1.5px; width: 8px; height: 16px; border-radius: 50%; border: 1.5px solid currentColor; display: block;"></span>
|
||||
</span>
|
||||
<span style="font-variant-numeric: tabular-nums;">{{ langLabel }}</span>
|
||||
</button>
|
||||
<div hidden="{{ langMenuOff }}" style="position: absolute; right: 0; top: 46px; width: 172px; padding: 6px; background: var(--surface); border: 1px solid var(--border); border-radius: 14px; box-shadow: var(--shadow); font-size: 14px;">
|
||||
<button type="button" onClick="{{ setRu }}" style="display: flex; align-items: center; justify-content: space-between; gap: 12px; width: 100%; padding: 9px 10px; border: 0; border-radius: 9px; background: {{ ruBg }}; color: var(--ink); font-family: inherit; font-size: 14px; text-align: left; cursor: pointer;">
|
||||
<span>Русский</span>
|
||||
<span style="color: var(--accent);">{{ ruCheck }}</span>
|
||||
</button>
|
||||
<button type="button" onClick="{{ setEn }}" style="display: flex; align-items: center; justify-content: space-between; gap: 12px; width: 100%; padding: 9px 10px; border: 0; border-radius: 9px; background: {{ enBg }}; color: var(--ink); font-family: inherit; font-size: 14px; text-align: left; cursor: pointer;">
|
||||
<span>English</span>
|
||||
<span style="color: var(--accent);">{{ enCheck }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<a href="MassApp.dc.html" hidden="{{ navOff }}" style="display: flex; align-items: center; justify-content: center; height: 38px; width: 78px; flex: none; box-sizing: border-box; border-radius: 10px; color: var(--ink-2); font-size: 15px;" style-hover="color: var(--ink); background: var(--surface-2);">Войти</a>
|
||||
<a href="#prices" hidden="{{ navOff }}" style="display: flex; align-items: center; justify-content: center; height: 38px; width: 142px; flex: none; box-sizing: border-box; border-radius: 10px; background: var(--accent); color: var(--on-accent); font-size: 15px; font-weight: 500; box-shadow: 0 6px 18px -8px var(--accent);" style-hover="background: var(--accent-hover); color: var(--on-accent);">Получить VPN</a>
|
||||
<button type="button" hidden="{{ burgerOff }}" onClick="{{ toggleMenu }}" title="Меню" style="display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 5px; width: 42px; height: 38px; border: 1px solid var(--border); border-radius: 10px; background: transparent; color: var(--ink); cursor: pointer;" style-hover="border-color: var(--ink-3);">
|
||||
<span style="width: 18px; height: 2px; border-radius: 2px; background: currentColor; display: block;"></span>
|
||||
<span style="width: 18px; height: 2px; border-radius: 2px; background: currentColor; display: block;"></span>
|
||||
<span style="width: 18px; height: 2px; border-radius: 2px; background: currentColor; display: block;"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div hidden="{{ menuOff }}" style="position: absolute; left: 0; right: 0; top: 100%; z-index: 45; padding: 10px 20px 18px; background: var(--bg); border-bottom: 1px solid var(--border); box-shadow: var(--shadow); display: grid; gap: 2px;">
|
||||
<a href="MassPrice.dc.html" onClick="{{ closeMenu }}" style="padding: 13px 6px; font-size: 17px; color: var(--ink);">Цены</a>
|
||||
<a href="MassLocations.dc.html" onClick="{{ closeMenu }}" style="padding: 13px 6px; font-size: 17px; color: var(--ink);">Локации</a>
|
||||
<a href="MassSupport.dc.html" onClick="{{ closeMenu }}" style="padding: 13px 6px; font-size: 17px; color: var(--ink);">Поддержка</a>
|
||||
<div style="display: flex; align-items: center; justify-content: space-between; gap: 12px; padding-top: 6px; margin-top: 6px; border-top: 1px solid var(--border);">
|
||||
<a href="MassApp.dc.html" onClick="{{ closeMenu }}" style="padding: 13px 6px; font-size: 17px; color: var(--ink);">Войти</a>
|
||||
<div style="display: flex; gap: 6px; flex: none;">
|
||||
<button type="button" onClick="{{ setRu }}" style="padding: 9px 13px; border: 1px solid var(--border); border-radius: 10px; background: {{ ruBg }}; color: var(--ink); font-family: inherit; font-size: 13px; cursor: pointer; white-space: nowrap;">Русский</button>
|
||||
<button type="button" onClick="{{ setEn }}" style="padding: 9px 13px; border: 1px solid var(--border); border-radius: 10px; background: {{ enBg }}; color: var(--ink); font-family: inherit; font-size: 13px; cursor: pointer; white-space: nowrap;">English</button>
|
||||
</div>
|
||||
</div>
|
||||
<a href="#prices" onClick="{{ closeMenu }}" style="display: flex; align-items: center; justify-content: center; height: 50px; margin-top: 12px; border-radius: 12px; background: var(--accent); color: var(--on-accent); font-size: 16px; font-weight: 500;">Получить VPN</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main id="top" style="max-width: 1200px; margin: 0 auto; padding: 0 24px;">
|
||||
|
||||
<section style="position: relative; min-height: 62vh; display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: clamp(28px, 5vw, 56px); align-items: center; padding: clamp(32px, 7vw, 72px) 0 clamp(28px, 7vw, 84px);">
|
||||
<div>
|
||||
<div style="display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin-bottom: 28px;">
|
||||
<div style="display: inline-flex; align-items: center; gap: 8px; padding: 6px 12px 6px 8px; border: 1px solid var(--border); border-radius: 999px; font-size: 13px; color: var(--ink-2);">
|
||||
<span style="width: 6px; height: 6px; border-radius: 50%; background: var(--accent); display: block;"></span>
|
||||
Все страны в одной подписке
|
||||
</div>
|
||||
<div style="display: inline-flex; align-items: center; padding: 6px 12px; border-radius: 999px; background: var(--accent-soft); color: var(--accent); font-size: 13px; font-weight: 500;">Работаем с 2022 года</div>
|
||||
</div>
|
||||
<h1 style="margin: 0 0 22px; font-size: clamp(40px, 5.2vw, 62px); line-height: 1.04; font-weight: 600; letter-spacing: -0.035em; text-wrap: balance;">VPN, который не нужно отключать</h1>
|
||||
<p style="margin: 0 0 34px; max-width: 30em; font-size: 18px; line-height: 1.55; color: var(--ink-2); font-weight: 300; text-wrap: pretty;">Тридцать локаций и российский адрес входят в подписку. Переключайтесь между странами сколько угодно, на пяти устройствах сразу.</p>
|
||||
<div style="display: flex; flex-wrap: wrap; align-items: center; gap: clamp(8px, 2vw, 12px);">
|
||||
<a href="#prices" style="display: flex; align-items: center; justify-content: center; height: 50px; padding: 0 clamp(14px, 4vw, 24px); border-radius: 12px; background: var(--accent); color: var(--on-accent); font-size: 16px; font-weight: 500; box-shadow: 0 10px 28px -12px var(--accent);" style-hover="background: var(--accent-hover); color: var(--on-accent);">Получить VPN</a>
|
||||
<a href="#locations" style="display: flex; align-items: center; justify-content: center; height: 50px; padding: 0 clamp(14px, 4vw, 22px); border-radius: 12px; border: 1px solid var(--border); color: var(--ink); font-size: 16px; white-space: nowrap;" style-hover="border-color: var(--ink-3); color: var(--ink);">Посмотреть локации</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="position: relative; aspect-ratio: 1 / 1; max-width: 480px; width: 100%; justify-self: {{ heroAlign }}; border-radius: 28px; overflow: hidden; isolation: isolate; -webkit-transform: translateZ(0); transform: translateZ(0); background: var(--surface-2); border: 1px solid var(--border);">
|
||||
<div style="position: absolute; left: -12%; top: -8%; width: 78%; height: 78%; border-radius: 50%; background: var(--glow-1); filter: blur(46px); opacity: 0.85; animation: veilDrift 16s ease-in-out infinite;"></div>
|
||||
<div style="position: absolute; right: -14%; top: 14%; width: 70%; height: 70%; border-radius: 50%; background: var(--glow-3); filter: blur(52px); opacity: 0.75; animation: veilDrift2 19s ease-in-out infinite;"></div>
|
||||
<div style="position: absolute; left: 16%; bottom: -16%; width: 82%; height: 68%; border-radius: 50%; background: var(--glow-2); filter: blur(54px); opacity: 0.8; animation: veilDrift 23s ease-in-out infinite;"></div>
|
||||
<div id="veilix-globe" style="position: absolute; left: 8%; top: 8%; width: 84%; height: 84%;"></div>
|
||||
<div style="position: absolute; left: 8%; top: 30%; right: 8%; height: 26%; border-radius: 18px; background: color-mix(in oklab, var(--veil) 72%, transparent); border: 1px solid color-mix(in oklab, var(--ink) 10%, transparent); backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px); transform: rotate(-8deg);"></div>
|
||||
<div style="position: absolute; left: 12%; top: 46%; right: 4%; height: 24%; border-radius: 18px; background: color-mix(in oklab, var(--veil) 72%, transparent); border: 1px solid color-mix(in oklab, var(--ink) 8%, transparent); backdrop-filter: blur(9px); -webkit-backdrop-filter: blur(9px); transform: rotate(6deg);"></div>
|
||||
<div style="position: absolute; left: 20px; right: 20px; bottom: 20px; display: grid; gap: 10px; padding: 14px 16px; border-radius: 16px; background: color-mix(in oklab, var(--surface) 84%, transparent); backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px); border: 1px solid var(--border);">
|
||||
<div style="display: flex; align-items: center; gap: 10px; font-size: 13px; color: var(--ink-2);">
|
||||
<span style="width: 7px; height: 7px; border-radius: 50%; background: var(--accent); display: block; flex: none;"></span>
|
||||
30 стран, переключение в один тап
|
||||
</div>
|
||||
<div style="display: flex; flex-wrap: wrap; gap: 6px 16px; padding-top: 10px; border-top: 1px solid var(--border); font-size: 13px; color: var(--ink-3); font-variant-numeric: tabular-nums;">
|
||||
<span>699 ₽ в месяц или 6 990 ₽ в год</span>
|
||||
<span>До 5 устройств</span>
|
||||
<span>RU-адрес включён</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="advantages" style="padding: 16px 0 8px;">
|
||||
<div style="display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; gap: 24px; margin-bottom: 28px;">
|
||||
<h2 style="margin: 0; font-size: clamp(28px, 3vw, 38px); font-weight: 600; letter-spacing: -0.03em;">Что это даёт на практике</h2>
|
||||
</div>
|
||||
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 16px;">
|
||||
<div style="position: relative; isolation: isolate; padding: clamp(20px, 5vw, 32px); border-radius: 22px; background: var(--surface); border: 1px solid var(--border); box-shadow: var(--shadow);">
|
||||
<div style="position: absolute; inset: 0; border-radius: 22px; overflow: hidden; pointer-events: none; -webkit-mask-image: -webkit-radial-gradient(white, black); mask-image: radial-gradient(white, black);">
|
||||
<div style="position: absolute; right: -80px; top: -80px; width: 260px; height: 260px; border-radius: 50%; background: var(--glow-1); filter: blur(60px); opacity: 0.5;"></div>
|
||||
</div>
|
||||
<div style="position: relative;">
|
||||
<div style="display: flex; align-items: center; gap: 12px; margin-bottom: 18px;">
|
||||
<span style="position: relative; width: 34px; height: 34px; flex: none; display: block;">
|
||||
<span style="position: absolute; left: 11px; top: 3px; width: 12px; height: 12px; border-radius: 50%; background: var(--accent); display: block;"></span>
|
||||
<span style="position: absolute; left: 4px; top: 19px; width: 26px; height: 14px; border-radius: 13px 13px 4px 4px; background: var(--accent); opacity: 0.35; display: block;"></span>
|
||||
<span style="position: absolute; left: 23px; top: 18px; width: 11px; height: 11px; border-radius: 50%; border: 2px solid var(--accent); background: var(--surface); display: block;"></span>
|
||||
</span>
|
||||
<h3 style="margin: 0; font-size: 24px; font-weight: 600; letter-spacing: -0.025em;">Все локации в подписке</h3>
|
||||
</div>
|
||||
<p style="margin: 0 0 26px; max-width: 34em; font-size: 16px; line-height: 1.55; color: var(--ink-2); font-weight: 300;">Список стран открыт целиком. Выбираете страну в приложении, соединение перестраивается за пару секунд, доплачивать за переключение не нужно.</p>
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px 28px;">
|
||||
<div>
|
||||
<div style="font-size: 15px; font-weight: 500; margin-bottom: 6px;">Тридцать стран</div>
|
||||
<div style="font-size: 14px; line-height: 1.5; color: var(--ink-2); font-weight: 300;">Европа, США, Азия, Ближний Восток и Южная Америка. Список пополняется.</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size: 15px; font-weight: 500; margin-bottom: 6px;">Пять устройств сразу</div>
|
||||
<div style="font-size: 14px; line-height: 1.5; color: var(--ink-2); font-weight: 300;">Телефон, ноутбук, планшет и ещё два устройства работают одновременно.</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size: 15px; font-weight: 500; margin-bottom: 6px;">Российские сайты напрямую</div>
|
||||
<div style="font-size: 14px; line-height: 1.5; color: var(--ink-2); font-weight: 300;">Банки и госуслуги работают как обычно, отключать VPN не нужно.</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size: 15px; font-weight: 500; margin-bottom: 6px;">Трафик под шифрованием</div>
|
||||
<div style="font-size: 14px; line-height: 1.5; color: var(--ink-2); font-weight: 300;">Дома, в отеле или в кафе соединение защищено в любой точке мира.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display: grid; gap: 16px; align-content: start;">
|
||||
<div style="padding: clamp(20px, 4.6vw, 26px); border-radius: 22px; background: var(--surface); border: 1px solid var(--border); box-shadow: var(--shadow);">
|
||||
<div style="display: flex; gap: 4px; margin-bottom: 16px;">
|
||||
<span style="width: 11px; height: 22px; border-radius: 3px; background: var(--accent); display: block;"></span>
|
||||
<span style="width: 11px; height: 22px; border-radius: 3px; background: color-mix(in oklab, var(--accent) 75%, transparent); display: block;"></span>
|
||||
<span style="width: 11px; height: 22px; border-radius: 3px; background: color-mix(in oklab, var(--accent) 55%, transparent); display: block;"></span>
|
||||
<span style="width: 11px; height: 22px; border-radius: 3px; background: color-mix(in oklab, var(--accent) 35%, transparent); display: block;"></span>
|
||||
<span style="width: 11px; height: 22px; border-radius: 3px; background: color-mix(in oklab, var(--accent) 18%, transparent); display: block;"></span>
|
||||
</div>
|
||||
<h3 style="margin: 0 0 8px; font-size: 19px; font-weight: 600; letter-spacing: -0.02em;">Пять устройств в подписке</h3>
|
||||
<p style="margin: 0; font-size: 14px; line-height: 1.5; color: var(--ink-2); font-weight: 300;">Одна подписка на всю технику. Устройства работают одновременно и в разных странах.</p>
|
||||
</div>
|
||||
|
||||
<div style="padding: clamp(20px, 4.6vw, 26px); border-radius: 22px; background: var(--surface); border: 1px solid var(--border); box-shadow: var(--shadow);">
|
||||
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 16px;">
|
||||
<span style="width: 22px; height: 22px; border-radius: 50%; background: var(--accent); display: block;"></span>
|
||||
<span style="width: 26px; height: 2px; background: color-mix(in oklab, var(--accent) 40%, transparent); display: block;"></span>
|
||||
<span style="width: 22px; height: 22px; border-radius: 50%; border: 2px solid color-mix(in oklab, var(--accent) 55%, transparent); display: block;"></span>
|
||||
</div>
|
||||
<h3 style="margin: 0 0 8px; font-size: 19px; font-weight: 600; letter-spacing: -0.02em;">Российский адрес включён</h3>
|
||||
<p style="margin: 0; font-size: 14px; line-height: 1.5; color: var(--ink-2); font-weight: 300;">Российская локация входит в подписку: банки, госуслуги и стриминг открываются как дома.</p>
|
||||
</div>
|
||||
|
||||
<div style="padding: clamp(20px, 4.6vw, 26px); border-radius: 22px; background: var(--surface); border: 1px solid var(--border); box-shadow: var(--shadow);">
|
||||
<div style="display: flex; align-items: center; gap: 6px; margin-bottom: 16px;">
|
||||
<span style="width: 22px; height: 22px; border-radius: 7px; background: var(--accent); transform: rotate(45deg); display: block;"></span>
|
||||
<span style="width: 22px; height: 22px; border-radius: 7px; background: var(--veil); border: 1px solid color-mix(in oklab, var(--accent) 40%, transparent); transform: rotate(45deg); display: block;"></span>
|
||||
</div>
|
||||
<h3 style="margin: 0 0 8px; font-size: 19px; font-weight: 600; letter-spacing: -0.02em;">Современный протокол XRAY</h3>
|
||||
<p style="margin: 0 0 12px; font-size: 14px; line-height: 1.5; color: var(--ink-2); font-weight: 300;">Трафик неотличим от обычного HTTPS, поэтому подключение держится там, где старые протоколы уже не работают.</p>
|
||||
<div style="display: flex; flex-wrap: wrap; gap: 6px; font-size: 12px; color: var(--ink-3); font-variant-numeric: tabular-nums;">
|
||||
<span style="padding: 4px 9px; border-radius: 7px; background: var(--surface-2); border: 1px solid var(--border);">VLESS</span>
|
||||
<span style="padding: 4px 9px; border-radius: 7px; background: var(--surface-2); border: 1px solid var(--border);">Hysteria2</span>
|
||||
<span style="padding: 4px 9px; border-radius: 7px; background: var(--surface-2); border: 1px solid var(--border);">XHTTP</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 16px; padding: clamp(20px, 5vw, 32px); border-radius: 22px; background: var(--surface); border: 1px solid var(--border); box-shadow: var(--shadow);">
|
||||
<h3 style="margin: 0 0 6px; font-size: 24px; font-weight: 600; letter-spacing: -0.025em;">Трафик идёт коротким путём</h3>
|
||||
<p style="margin: 0 0 28px; max-width: 46em; font-size: 16px; line-height: 1.55; color: var(--ink-2); font-weight: 300;">Маршрут выбирается сам, по адресу сайта. Российские сервисы не ломаются, зарубежные открываются, переключать ничего не нужно.</p>
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 16px;">
|
||||
<div style="padding: 22px; border-radius: 16px; background: var(--surface-2); border: 1px solid var(--border);">
|
||||
<div style="font-size: 13px; color: var(--ink-3); margin-bottom: 16px;">Вы в России</div>
|
||||
<div style="display: grid; gap: 12px;">
|
||||
<div style="display: flex; flex-wrap: wrap; align-items: center; gap: 6px 12px; font-size: 14px;">
|
||||
<span style="flex: none; width: 72px; color: var(--ink-2); font-weight: 500;">RU-сайты</span>
|
||||
<span style="flex: 1; height: 1px; background: linear-gradient(90deg, var(--accent), color-mix(in oklab, var(--accent) 25%, transparent)); display: block;"></span>
|
||||
<span style="flex: none; color: var(--ink-2);">напрямую через RU-сервер</span>
|
||||
</div>
|
||||
<div style="display: flex; flex-wrap: wrap; align-items: center; gap: 6px 12px; font-size: 14px;">
|
||||
<span style="flex: none; width: 72px; color: var(--ink-2); font-weight: 500;">Остальной мир</span>
|
||||
<span style="flex: 1; height: 1px; background: repeating-linear-gradient(90deg, var(--accent) 0 6px, transparent 6px 12px); display: block;"></span>
|
||||
<span style="flex: none; color: var(--ink-2);">туннелем в выбранную локацию</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="padding: 22px; border-radius: 16px; background: var(--surface-2); border: 1px solid var(--border);">
|
||||
<div style="font-size: 13px; color: var(--ink-3); margin-bottom: 16px;">Вы за границей</div>
|
||||
<div style="display: grid; gap: 12px;">
|
||||
<div style="display: flex; flex-wrap: wrap; align-items: center; gap: 6px 12px; font-size: 14px;">
|
||||
<span style="flex: none; width: 72px; color: var(--ink-2); font-weight: 500;">Мировые сайты</span>
|
||||
<span style="flex: 1; height: 1px; background: linear-gradient(90deg, var(--accent), color-mix(in oklab, var(--accent) 25%, transparent)); display: block;"></span>
|
||||
<span style="flex: none; color: var(--ink-2);">прямо из выбранной локации</span>
|
||||
</div>
|
||||
<div style="display: flex; flex-wrap: wrap; align-items: center; gap: 6px 12px; font-size: 14px;">
|
||||
<span style="flex: none; width: 72px; color: var(--ink-2); font-weight: 500;">RU-сайты</span>
|
||||
<span style="flex: 1; height: 1px; background: repeating-linear-gradient(90deg, var(--accent) 0 6px, transparent 6px 12px); display: block;"></span>
|
||||
<span style="flex: none; color: var(--ink-2);">туннелем через RU-сервер</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-top: 16px; font-size: 13px; color: var(--ink-3);">Для тех, кто работает с Россией из другой страны.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="prices" style="padding: 72px 0 8px;">
|
||||
<div style="position: relative; overflow: hidden; isolation: isolate; -webkit-mask-image: -webkit-radial-gradient(white, black); mask-image: radial-gradient(white, black); -webkit-transform: translateZ(0); transform: translateZ(0); border-radius: 26px; border: 1px solid var(--border); background: var(--surface); box-shadow: var(--shadow);">
|
||||
<div style="position: absolute; left: min(-60px, calc(-110px + 8vw)); bottom: min(-120px, calc(-170px + 10vw)); width: clamp(150px, 34vw, 340px); height: clamp(150px, 34vw, 340px); border-radius: 50%; background: radial-gradient(circle at 60% 40%, var(--glow-1) 0%, color-mix(in oklab, var(--glow-1) 35%, transparent) 55%, transparent 78%); filter: blur(70px); opacity: 0.45;"></div>
|
||||
<div style="position: relative; display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: clamp(28px, 5vw, 40px); padding: clamp(20px, 5vw, 40px);">
|
||||
<div>
|
||||
<h2 style="margin: 0 0 10px; font-size: clamp(26px, 3vw, 34px); font-weight: 600; letter-spacing: -0.03em;">Одна подписка, все страны</h2>
|
||||
<p style="margin: 0 0 22px; max-width: 34em; font-size: 16px; line-height: 1.55; color: var(--ink-2); font-weight: 300;">Тридцать локаций, российский адрес и пять устройств.</p>
|
||||
<div style="display: grid; gap: 10px; font-size: 14px; color: var(--ink-2); font-weight: 300; margin-bottom: 24px;">
|
||||
<div style="display: flex; gap: 10px;"><span style="color: var(--accent);">—</span>Все 30 стран и российский адрес</div>
|
||||
<div style="display: flex; gap: 10px;"><span style="color: var(--accent);">—</span>До 5 устройств одновременно</div>
|
||||
<div style="display: flex; gap: 10px;"><span style="color: var(--accent);">—</span>Современный протокол XRAY</div>
|
||||
</div>
|
||||
<div style="font-size: 13px; color: var(--ink-3); margin-bottom: 10px;">Страны в подписке</div>
|
||||
<div style="display: flex; flex-wrap: wrap; gap: 6px;">
|
||||
<sc-for list="{{ countries }}" as="c" hint-placeholder-count="14">
|
||||
<span style="padding: 5px 10px; border-radius: 8px; background: var(--surface-2); border: 1px solid var(--border); font-size: 13px; color: var(--ink-2);">{{ c.name }}</span>
|
||||
</sc-for>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display: grid; gap: 12px; align-content: start;">
|
||||
<div role="button" tabindex="0" onClick="{{ pickMonth }}" style="display: grid; gap: 6px; padding: 22px; border-radius: 18px; border: 1px solid {{ planMonthBorder }}; background: {{ planMonthBg }}; cursor: pointer;" style-hover="border-color: var(--ink-3);">
|
||||
<span style="display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; gap: 10px;">
|
||||
<span style="font-size: 16px; font-weight: 500;">Месяц</span>
|
||||
<span style="font-size: 26px; font-weight: 600; letter-spacing: -0.03em; font-variant-numeric: tabular-nums;">699 ₽</span>
|
||||
</span>
|
||||
<span style="font-size: 14px; color: var(--ink-3); font-weight: 300;">Списание раз в месяц, отмена в любой момент</span>
|
||||
</div>
|
||||
<div role="button" tabindex="0" onClick="{{ pickYear }}" style="display: grid; gap: 6px; padding: 22px; border-radius: 18px; border: 1px solid {{ planYearBorder }}; background: {{ planYearBg }}; cursor: pointer;" style-hover="border-color: var(--ink-3);">
|
||||
<span style="display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; gap: 10px;">
|
||||
<span style="display: inline-flex; align-items: center; gap: 8px; font-size: 16px; font-weight: 500;">Год
|
||||
<span style="padding: 3px 8px; border-radius: 7px; background: var(--accent-soft); color: var(--accent); font-size: 12px; font-weight: 500;">выгоднее</span>
|
||||
</span>
|
||||
<span style="font-size: 26px; font-weight: 600; letter-spacing: -0.03em; font-variant-numeric: tabular-nums;">6 990 ₽</span>
|
||||
</span>
|
||||
<span style="font-size: 14px; color: var(--ink-3); font-weight: 300; font-variant-numeric: tabular-nums;">582 ₽ в месяц, экономия 1 398 ₽ за год</span>
|
||||
</div>
|
||||
<a href="MassOrder.dc.html" style="display: flex; align-items: center; justify-content: center; height: 50px; margin-top: 4px; border-radius: 12px; background: var(--accent); color: var(--on-accent); font-size: 16px; font-weight: 500; box-shadow: 0 10px 28px -12px var(--accent);" style-hover="background: var(--accent-hover); color: var(--on-accent);">Оформить подписку</a>
|
||||
<div style="font-size: 13px; color: var(--ink-3); line-height: 1.5;">Карты российских и зарубежных банков, криптовалюта. Доступ открывается сразу после оплаты.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section style="padding: 24px 0 0;">
|
||||
<div style="display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 18px; padding: clamp(20px, 5vw, 32px); border-radius: 22px; border: 1px solid var(--border); background: var(--surface-2);">
|
||||
<div>
|
||||
<h3 style="margin: 0 0 8px; font-size: 20px; font-weight: 600; letter-spacing: -0.025em;">Нужен адрес, закреплённый только за вами</h3>
|
||||
<p style="margin: 0; max-width: 46em; font-size: 15px; line-height: 1.55; color: var(--ink-2); font-weight: 300;">Здесь сервер общий: он дешевле и этого достаточно большинству. Если сервисы требуют постоянный IP, посмотрите индивидуальные тарифы Вейликс.</p>
|
||||
</div>
|
||||
<a href="Main.dc.html" style="display: flex; align-items: center; justify-content: center; height: 48px; padding: 0 22px; border-radius: 12px; border: 1px solid var(--accent); color: var(--accent); font-size: 15px; font-weight: 500; white-space: nowrap;" style-hover="background: var(--accent-soft); color: var(--accent);">Индивидуальный IP</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="faq" style="padding: 72px 0 32px;">
|
||||
<h2 style="margin: 0 0 28px; font-size: clamp(28px, 3vw, 38px); font-weight: 600; letter-spacing: -0.03em;">Вопросы</h2>
|
||||
<div style="display: grid; gap: 10px; max-width: 860px;">
|
||||
|
||||
<div style="border: 1px solid var(--border); border-radius: 16px; background: var(--surface); overflow: hidden;">
|
||||
<button type="button" onClick="{{ faq0 }}" style="display: flex; align-items: center; gap: 16px; width: 100%; padding: 20px 22px; background: transparent; border: 0; color: var(--ink); font-family: inherit; font-size: 17px; font-weight: 500; text-align: left; cursor: pointer; letter-spacing: -0.015em;">
|
||||
<span style="flex: 1;">Что входит в подписку?</span>
|
||||
<span style="flex: none; width: 10px; height: 10px; border-right: 1.5px solid var(--ink-3); border-bottom: 1.5px solid var(--ink-3); transform: {{ faq0Rot }}; transition: transform 180ms ease; display: block;"></span>
|
||||
</button>
|
||||
<div hidden="{{ faq0Off }}" style="padding: 0 22px 22px; font-size: 15px; line-height: 1.6; color: var(--ink-2); font-weight: 300; max-width: 62ch;">Все тридцать стран, включая российскую локацию, и пять устройств. Отдельной платы за страну или устройство нет.</div>
|
||||
</div>
|
||||
|
||||
<div style="border: 1px solid var(--border); border-radius: 16px; background: var(--surface); overflow: hidden;">
|
||||
<button type="button" onClick="{{ faq1 }}" style="display: flex; align-items: center; gap: 16px; width: 100%; padding: 20px 22px; background: transparent; border: 0; color: var(--ink); font-family: inherit; font-size: 17px; font-weight: 500; text-align: left; cursor: pointer; letter-spacing: -0.015em;">
|
||||
<span style="flex: 1;">Можно менять страну сколько угодно?</span>
|
||||
<span style="flex: none; width: 10px; height: 10px; border-right: 1.5px solid var(--ink-3); border-bottom: 1.5px solid var(--ink-3); transform: {{ faq1Rot }}; transition: transform 180ms ease; display: block;"></span>
|
||||
</button>
|
||||
<div hidden="{{ faq1Off }}" style="padding: 0 22px 22px; font-size: 15px; line-height: 1.6; color: var(--ink-2); font-weight: 300; max-width: 62ch;">Да. Переключение занимает пару секунд и ничего не стоит: список локаций открыт целиком на всё время подписки.</div>
|
||||
</div>
|
||||
|
||||
<div style="border: 1px solid var(--border); border-radius: 16px; background: var(--surface); overflow: hidden;">
|
||||
<button type="button" onClick="{{ faq2 }}" style="display: flex; align-items: center; gap: 16px; width: 100%; padding: 20px 22px; background: transparent; border: 0; color: var(--ink); font-family: inherit; font-size: 17px; font-weight: 500; text-align: left; cursor: pointer; letter-spacing: -0.015em;">
|
||||
<span style="flex: 1;">Нужно ли что-то настраивать самому?</span>
|
||||
<span style="flex: none; width: 10px; height: 10px; border-right: 1.5px solid var(--ink-3); border-bottom: 1.5px solid var(--ink-3); transform: {{ faq2Rot }}; transition: transform 180ms ease; display: block;"></span>
|
||||
</button>
|
||||
<div hidden="{{ faq2Off }}" style="padding: 0 22px 22px; font-size: 15px; line-height: 1.6; color: var(--ink-2); font-weight: 300; max-width: 62ch;">Нет. В кабинете вы скачиваете готовый файл или сканируете QR-код с телефона. Российские сайты идут напрямую, зарубежные — через выбранную локацию.</div>
|
||||
</div>
|
||||
|
||||
<div style="border: 1px solid var(--border); border-radius: 16px; background: var(--surface); overflow: hidden;">
|
||||
<button type="button" onClick="{{ faq3 }}" style="display: flex; align-items: center; gap: 16px; width: 100%; padding: 20px 22px; background: transparent; border: 0; color: var(--ink); font-family: inherit; font-size: 17px; font-weight: 500; text-align: left; cursor: pointer; letter-spacing: -0.015em;">
|
||||
<span style="flex: 1;">На каком протоколе работает подключение?</span>
|
||||
<span style="flex: none; width: 10px; height: 10px; border-right: 1.5px solid var(--ink-3); border-bottom: 1.5px solid var(--ink-3); transform: {{ faq3Rot }}; transition: transform 180ms ease; display: block;"></span>
|
||||
</button>
|
||||
<div hidden="{{ faq3Off }}" style="padding: 0 22px 22px; font-size: 15px; line-height: 1.6; color: var(--ink-2); font-weight: 300; max-width: 62ch;">На XRAY — это современный протокол с VLESS, Hysteria2 и XHTTP. Трафик выглядит как обычный HTTPS, поэтому соединение держится там, где старые протоколы блокируются.</div>
|
||||
</div>
|
||||
|
||||
<div style="border: 1px solid var(--border); border-radius: 16px; background: var(--surface); overflow: hidden;">
|
||||
<button type="button" onClick="{{ faq4 }}" style="display: flex; align-items: center; gap: 16px; width: 100%; padding: 20px 22px; background: transparent; border: 0; color: var(--ink); font-family: inherit; font-size: 17px; font-weight: 500; text-align: left; cursor: pointer; letter-spacing: -0.015em;">
|
||||
<span style="flex: 1;">Чем это отличается от индивидуального тарифа?</span>
|
||||
<span style="flex: none; width: 10px; height: 10px; border-right: 1.5px solid var(--ink-3); border-bottom: 1.5px solid var(--ink-3); transform: {{ faq4Rot }}; transition: transform 180ms ease; display: block;"></span>
|
||||
</button>
|
||||
<div hidden="{{ faq4Off }}" style="padding: 0 22px 22px; font-size: 15px; line-height: 1.6; color: var(--ink-2); font-weight: 300; max-width: 62ch;">Здесь адрес общий: на сервере несколько человек, поэтому подписка стоит заметно дешевле. Если нужен адрес, закреплённый только за вами, это отдельные тарифы с индивидуальным IP.</div>
|
||||
</div>
|
||||
|
||||
<div style="border: 1px solid var(--border); border-radius: 16px; background: var(--surface); overflow: hidden;">
|
||||
<button type="button" onClick="{{ faq5 }}" style="display: flex; align-items: center; gap: 16px; width: 100%; padding: 20px 22px; background: transparent; border: 0; color: var(--ink); font-family: inherit; font-size: 17px; font-weight: 500; text-align: left; cursor: pointer; letter-spacing: -0.015em;">
|
||||
<span style="flex: 1;">Как оплачивается и продлевается?</span>
|
||||
<span style="flex: none; width: 10px; height: 10px; border-right: 1.5px solid var(--ink-3); border-bottom: 1.5px solid var(--ink-3); transform: {{ faq5Rot }}; transition: transform 180ms ease; display: block;"></span>
|
||||
</button>
|
||||
<div hidden="{{ faq5Off }}" style="padding: 0 22px 22px; font-size: 15px; line-height: 1.6; color: var(--ink-2); font-weight: 300; max-width: 62ch;">699 ₽ в месяц или 6 990 ₽ в год со списанием с привязанной карты. Годовая подписка дешевле помесячной на 1 398 ₽. Отменить продление можно в кабинете в любой момент.</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</main>
|
||||
|
||||
<footer style="margin-top: 40px; border-top: 1px solid var(--border); background: var(--surface-2);">
|
||||
<div style="max-width: 1200px; margin: 0 auto; padding: 48px clamp(16px, 4vw, 24px) 32px; display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 32px;">
|
||||
<div>
|
||||
<div style="display: flex; align-items: center; gap: 10px; margin-bottom: 14px;">
|
||||
<span style="position: relative; width: 24px; height: 24px; display: block; flex: none;">
|
||||
<span hidden="{{ mark1Off }}" style="position: absolute; inset: 0;">
|
||||
<span style="position: absolute; left: 0; top: 3px; width: 18px; height: 18px; border-radius: 6px; background: linear-gradient(140deg, var(--accent), color-mix(in oklab, var(--accent) 55%, var(--glow-3))); display: block;"></span>
|
||||
<span style="position: absolute; right: 0; bottom: 3px; width: 18px; height: 18px; border-radius: 6px; background: var(--veil); border: 1px solid color-mix(in oklab, var(--accent) 35%, transparent); display: block;"></span>
|
||||
</span>
|
||||
<span hidden="{{ mark2Off }}" style="position: absolute; inset: 0; overflow: hidden;">
|
||||
<span style="position: absolute; left: 2px; top: 2px; width: 20px; height: 20px; border-radius: 50%; background: linear-gradient(150deg, var(--accent), color-mix(in oklab, var(--accent) 50%, var(--glow-2))); display: block;"></span>
|
||||
<span style="position: absolute; left: -5px; top: 11px; width: 34px; height: 15px; border-radius: 9px 9px 4px 4px; background: color-mix(in oklab, var(--surface-2) 72%, transparent); backdrop-filter: blur(3px); transform: rotate(-14deg); display: block;"></span>
|
||||
</span>
|
||||
<span hidden="{{ mark3Off }}" style="position: absolute; inset: 0;">
|
||||
<span style="position: absolute; left: 1px; top: 1px; width: 22px; height: 22px; border-radius: 50%; border: 2px solid var(--accent); border-bottom-color: transparent; border-right-color: transparent; transform: rotate(-30deg); display: block;"></span>
|
||||
<span style="position: absolute; left: 6px; top: 6px; width: 12px; height: 12px; border-radius: 50%; border: 2px solid color-mix(in oklab, var(--accent) 45%, transparent); border-bottom-color: transparent; border-right-color: transparent; transform: rotate(20deg); display: block;"></span>
|
||||
</span>
|
||||
<span hidden="{{ mark4Off }}" style="position: absolute; inset: 0;">
|
||||
<span style="position: absolute; left: 3px; top: 3px; width: 18px; height: 18px; border-radius: 5px; background: linear-gradient(135deg, var(--accent), color-mix(in oklab, var(--accent) 45%, var(--glow-1))); transform: rotate(45deg); display: block;"></span>
|
||||
<span style="position: absolute; left: 8px; top: 8px; width: 8px; height: 8px; border-radius: 3px; background: color-mix(in oklab, var(--surface-2) 85%, transparent); transform: rotate(45deg); display: block;"></span>
|
||||
</span>
|
||||
</span>
|
||||
<span style="font-family: 'Unbounded', 'Commissioner', sans-serif; font-weight: 600; font-size: 15px; letter-spacing: -0.02em;">ВейликсВПН</span>
|
||||
</div>
|
||||
<p style="margin: 0 0 20px; max-width: 26em; font-size: 14px; line-height: 1.55; color: var(--ink-3); font-weight: 300;">VPN, который не нужно отключать. Работаем с 2022 года.</p>
|
||||
<div style="display: flex; flex-wrap: wrap; gap: 6px;">
|
||||
<span style="display: flex; align-items: center; justify-content: center; width: 60px; height: 28px; border-radius: 8px; border: 1px solid var(--border); background: var(--surface); font-size: 11px; letter-spacing: 0.06em; color: var(--ink-3);">VISA</span>
|
||||
<span style="display: flex; align-items: center; justify-content: center; width: 60px; height: 28px; border-radius: 8px; border: 1px solid var(--border); background: var(--surface); font-size: 11px; letter-spacing: 0.06em; color: var(--ink-3);">MC</span>
|
||||
<span style="display: flex; align-items: center; justify-content: center; width: 60px; height: 28px; border-radius: 8px; border: 1px solid var(--border); background: var(--surface); font-size: 11px; letter-spacing: 0.06em; color: var(--ink-3);">МИР</span>
|
||||
<span style="display: flex; align-items: center; justify-content: center; width: 60px; height: 28px; border-radius: 8px; border: 1px solid var(--border); background: var(--surface); font-size: 11px; letter-spacing: 0.06em; color: var(--ink-3);">CRYPTO</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display: grid; grid-template-columns: repeat(2, minmax(130px, 1fr)); gap: 24px 32px; align-content: start;">
|
||||
<div style="display: grid; gap: 10px; align-content: start; font-size: 14px;">
|
||||
<div style="font-size: 13px; color: var(--ink-3); margin-bottom: 2px;">Сервис</div>
|
||||
<a href="MassPrice.dc.html" style="color: var(--ink-2);" style-hover="color: var(--ink);">Цены</a>
|
||||
<a href="MassLocations.dc.html" style="color: var(--ink-2);" style-hover="color: var(--ink);">Локации</a>
|
||||
<a href="MassSupport.dc.html" style="color: var(--ink-2);" style-hover="color: var(--ink);">Поддержка</a>
|
||||
</div>
|
||||
|
||||
<div style="display: grid; gap: 10px; align-content: start; font-size: 14px;">
|
||||
<div style="font-size: 13px; color: var(--ink-3); margin-bottom: 2px;">Документы</div>
|
||||
<a href="#top" style="color: var(--ink-2);" style-hover="color: var(--ink);">Условия использования</a>
|
||||
<a href="#top" style="color: var(--ink-2);" style-hover="color: var(--ink);">Политика конфиденциальности</a>
|
||||
<a href="#top" style="color: var(--ink-2);" style-hover="color: var(--ink);">Возврат средств</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div style="max-width: 1200px; margin: 0 auto; padding: 0 clamp(16px, 4vw, 24px) 40px; display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 14px;">
|
||||
<div style="font-size: 13px; color: var(--ink-3);">© 2026 ВейликсВПН</div>
|
||||
<div style="display: flex; align-items: center; gap: 8px;">
|
||||
<div style="display: flex; padding: 2px; border-radius: 9px; border: 1px solid var(--border); background: var(--surface); flex: none;">
|
||||
<button type="button" onClick="{{ setLight }}" title="Светлая тема" style="display: flex; align-items: center; justify-content: center; width: 28px; height: 28px; border: 0; border-radius: 7px; background: {{ lightBg }}; color: {{ lightInk }}; cursor: pointer;"><span style="position: relative; width: 14px; height: 14px; display: block;"><span style="position: absolute; left: 3px; top: 3px; width: 8px; height: 8px; border-radius: 50%; background: currentColor; display: block;"></span><span style="position: absolute; left: 0; top: 0; width: 14px; height: 14px; border-radius: 50%; border: 1.5px solid currentColor; opacity: 0.45; display: block;"></span></span></button>
|
||||
<button type="button" onClick="{{ setDark }}" title="Тёмная тема" style="display: flex; align-items: center; justify-content: center; width: 28px; height: 28px; border: 0; border-radius: 7px; background: {{ darkBg }}; color: {{ darkInk }}; cursor: pointer;"><span style="width: 14px; height: 14px; border-radius: 50%; box-shadow: inset -5px -1.5px 0 0 currentColor; display: block;"></span></button>
|
||||
<button type="button" onClick="{{ setSystem }}" title="Как в системе" style="display: flex; align-items: center; justify-content: center; width: 28px; height: 28px; border: 0; border-radius: 7px; background: {{ systemBg }}; color: {{ systemInk }}; cursor: pointer;"><span style="width: 14px; height: 14px; border-radius: 50%; border: 1.5px solid currentColor; background: linear-gradient(90deg, currentColor 50%, transparent 50%); display: block;"></span></button>
|
||||
</div>
|
||||
<div style="display: flex; padding: 2px; border-radius: 9px; border: 1px solid var(--border); background: var(--surface); flex: none;">
|
||||
<button type="button" onClick="{{ setRu }}" style="padding: 0 10px; height: 28px; border: 0; border-radius: 7px; background: {{ ruBg }}; color: {{ ruInk }}; font-family: inherit; font-size: 12px; cursor: pointer;">RU</button>
|
||||
<button type="button" onClick="{{ setEn }}" style="padding: 0 10px; height: 28px; border: 0; border-radius: 7px; background: {{ enBg }}; color: {{ enInk }}; font-family: inherit; font-size: 12px; cursor: pointer;">EN</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</x-dc>
|
||||
<script type="text/x-dc" data-dc-script data-props="{"defaultTheme":{"editor":"enum","options":["light","dark","system"],"default":"light","tsType":"'light' | 'dark' | 'system'","section":"Тема"},"logoMark":{"editor":"enum","options":["layers","veil","arcs","aperture"],"default":"layers","tsType":"string","section":"Логотип"},"showLogoPicker":{"editor":"boolean","default":false,"tsType":"boolean","section":"Логотип"}}">
|
||||
class Component extends DCLogic {
|
||||
state = { plan: "year", theme: null, sysDark: false, loc: 0, faq: 0, mark: null, langMenu: false, lang: "ru", narrow: false, stacked: false, menu: false, lead: false, leadSent: false };
|
||||
|
||||
data() {
|
||||
const base = { setup: "4 900 ₽", month: "2 499 ₽" };
|
||||
const rare = { setup: "6 900 ₽", month: "3 499 ₽" };
|
||||
const far = { setup: "9 900 ₽", month: "4 999 ₽" };
|
||||
const L = (country, city, code, lng, lat, ready, tier) => ({ country, city, code, lng, lat, ready, setup: tier.setup, month: tier.month, rare: tier !== base });
|
||||
return [
|
||||
L("Нидерланды", "Амстердам", "nl", 4.9, 52.37, true, base),
|
||||
L("Германия", "Франкфурт", "de", 8.68, 50.11, true, base),
|
||||
L("США", "Нью-Йорк", "us", -74.01, 40.71, true, base),
|
||||
L("Великобритания", "Лондон", "gb", -0.13, 51.51, true, base),
|
||||
L("Франция", "Париж", "fr", 2.35, 48.86, true, base),
|
||||
L("Финляндия", "Хельсинки", "fi", 24.94, 60.17, true, base),
|
||||
L("Швеция", "Стокгольм", "se", 18.07, 59.33, true, base),
|
||||
L("Норвегия", "Осло", "no", 10.75, 59.91, false, base),
|
||||
L("Дания", "Копенгаген", "dk", 12.57, 55.68, false, base),
|
||||
L("Швейцария", "Цюрих", "ch", 8.54, 47.37, true, rare),
|
||||
L("Австрия", "Вена", "at", 16.37, 48.21, true, base),
|
||||
L("Испания", "Мадрид", "es", -3.7, 40.42, true, base),
|
||||
L("Италия", "Милан", "it", 9.19, 45.46, true, base),
|
||||
L("Польша", "Варшава", "pl", 21.01, 52.23, true, base),
|
||||
L("Чехия", "Прага", "cz", 14.44, 50.08, true, base),
|
||||
L("Румыния", "Бухарест", "ro", 26.1, 44.43, false, base),
|
||||
L("Литва", "Вильнюс", "lt", 25.28, 54.69, true, base),
|
||||
L("Латвия", "Рига", "lv", 24.11, 56.95, true, base),
|
||||
L("Эстония", "Таллин", "ee", 24.75, 59.44, false, base),
|
||||
L("Турция", "Стамбул", "tr", 28.98, 41.01, true, base),
|
||||
L("Казахстан", "Алматы", "kz", 76.89, 43.24, false, base),
|
||||
L("Армения", "Ереван", "am", 44.51, 40.18, false, base),
|
||||
L("Грузия", "Тбилиси", "ge", 44.8, 41.72, false, base),
|
||||
L("ОАЭ", "Дубай", "ae", 55.27, 25.2, true, rare),
|
||||
L("Сингапур", "Сингапур", "sg", 103.82, 1.35, true, rare),
|
||||
L("Япония", "Токио", "jp", 139.69, 35.69, true, rare),
|
||||
L("Южная Корея", "Сеул", "kr", 126.98, 37.57, false, rare),
|
||||
L("Канада", "Торонто", "ca", -79.38, 43.65, true, base),
|
||||
L("Австралия", "Сидней", "au", 151.21, -33.87, false, far),
|
||||
L("Бразилия", "Сан-Паулу", "br", -46.63, -23.55, false, far)
|
||||
];
|
||||
}
|
||||
|
||||
flagSpecs() {
|
||||
return {
|
||||
nl: { rows: ["#AE1C28", "#FFFFFF", "#21468B"] },
|
||||
de: { rows: ["#000000", "#DD0000", "#FFCE00"] },
|
||||
us: { rows: ["#B22234", "#FFFFFF", "#B22234", "#FFFFFF", "#B22234", "#FFFFFF"], marks: [{ l: 0, t: 0, w: 22, h: 17, bg: "#3C3B6E" }] },
|
||||
gb: { bg: "#012169", marks: [{ l: 17, t: 0, w: 12, h: 32, bg: "#FFFFFF" }, { l: 0, t: 10, w: 46, h: 12, bg: "#FFFFFF" }, { l: 20, t: 0, w: 6, h: 32, bg: "#C8102E" }, { l: 0, t: 13, w: 46, h: 6, bg: "#C8102E" }] },
|
||||
fr: { cols: ["#002395", "#FFFFFF", "#ED2939"] },
|
||||
fi: { bg: "#FFFFFF", marks: [{ l: 13, t: 0, w: 8, h: 32, bg: "#003580" }, { l: 0, t: 12, w: 46, h: 8, bg: "#003580" }] },
|
||||
se: { bg: "#006AA7", marks: [{ l: 13, t: 0, w: 6, h: 32, bg: "#FECC00" }, { l: 0, t: 13, w: 46, h: 6, bg: "#FECC00" }] },
|
||||
no: { bg: "#BA0C2F", marks: [{ l: 12, t: 0, w: 10, h: 32, bg: "#FFFFFF" }, { l: 0, t: 11, w: 46, h: 10, bg: "#FFFFFF" }, { l: 15, t: 0, w: 4, h: 32, bg: "#00205B" }, { l: 0, t: 14, w: 46, h: 4, bg: "#00205B" }] },
|
||||
dk: { bg: "#C8102E", marks: [{ l: 13, t: 0, w: 6, h: 32, bg: "#FFFFFF" }, { l: 0, t: 13, w: 46, h: 6, bg: "#FFFFFF" }] },
|
||||
ch: { bg: "#D52B1E", marks: [{ l: 20, t: 8, w: 6, h: 16, bg: "#FFFFFF" }, { l: 15, t: 13, w: 16, h: 6, bg: "#FFFFFF" }] },
|
||||
at: { rows: ["#ED2939", "#FFFFFF", "#ED2939"] },
|
||||
es: { rows: ["#AA151B", "#F1BF00", "#AA151B"], sizes: "1fr 2fr 1fr" },
|
||||
it: { cols: ["#008C45", "#F4F5F0", "#CD212A"] },
|
||||
pl: { rows: ["#FFFFFF", "#DC143C"] },
|
||||
cz: { rows: ["#FFFFFF", "#D7141A"], marks: [{ l: 0, t: 0, w: 18, h: 32, bg: "#11457E", clip: "polygon(0 0, 100% 50%, 0 100%)" }] },
|
||||
ro: { cols: ["#002B7F", "#FCD116", "#CE1126"] },
|
||||
lt: { rows: ["#FDB913", "#006A44", "#C1272D"] },
|
||||
lv: { rows: ["#9E3039", "#FFFFFF", "#9E3039"], sizes: "2fr 1fr 2fr" },
|
||||
ee: { rows: ["#0072CE", "#000000", "#FFFFFF"] },
|
||||
tr: { bg: "#E30A17", marks: [{ l: 11, t: 8, w: 16, h: 16, r: "50%", bg: "#FFFFFF" }, { l: 16, t: 8, w: 16, h: 16, r: "50%", bg: "#E30A17" }] },
|
||||
kz: { bg: "#00AFCA", marks: [{ l: 16, t: 8, w: 15, h: 15, r: "50%", bg: "#FEC50C" }] },
|
||||
am: { rows: ["#D90012", "#0033A0", "#F2A800"] },
|
||||
ge: { bg: "#FFFFFF", marks: [{ l: 20, t: 0, w: 6, h: 32, bg: "#FF0000" }, { l: 0, t: 13, w: 46, h: 6, bg: "#FF0000" }] },
|
||||
ae: { rows: ["#00732F", "#FFFFFF", "#000000"], marks: [{ l: 0, t: 0, w: 11, h: 32, bg: "#FF0000" }] },
|
||||
sg: { rows: ["#EF3340", "#FFFFFF"], marks: [{ l: 5, t: 3, w: 11, h: 11, r: "50%", bg: "#FFFFFF" }, { l: 9, t: 3, w: 11, h: 11, r: "50%", bg: "#EF3340" }] },
|
||||
jp: { bg: "#FFFFFF", marks: [{ l: 16, t: 9, w: 14, h: 14, r: "50%", bg: "#BC002D" }] },
|
||||
kr: { bg: "#FFFFFF", marks: [{ l: 16, t: 9, w: 14, h: 14, r: "50%", bg: "#CD2E3A" }, { l: 16, t: 9, w: 14, h: 14, r: "50%", bg: "#0047A0", clip: "polygon(0 50%, 100% 50%, 100% 100%, 0 100%)" }] },
|
||||
ca: { cols: ["#D80621", "#FFFFFF", "#D80621"], sizes: "1fr 2fr 1fr", marks: [{ l: 20, t: 9, w: 7, h: 13, bg: "#D80621", clip: "polygon(50% 0, 100% 100%, 0 100%)" }] },
|
||||
au: { bg: "#00247D", marks: [{ l: 9, t: 0, w: 4, h: 16, bg: "#FFFFFF" }, { l: 0, t: 6, w: 22, h: 4, bg: "#FFFFFF" }, { l: 30, t: 19, w: 7, h: 7, r: "50%", bg: "#FFFFFF" }] },
|
||||
br: { bg: "#009739", marks: [{ l: 9, t: 4, w: 28, h: 24, bg: "#FEDD00", clip: "polygon(50% 0, 100% 50%, 50% 100%, 0 50%)" }, { l: 18, t: 11, w: 10, h: 10, r: "50%", bg: "#012169" }] }
|
||||
};
|
||||
}
|
||||
|
||||
flag(code) {
|
||||
const h = React.createElement;
|
||||
const spec = this.flagSpecs()[code] || {};
|
||||
const style = {
|
||||
width: "46px", height: "32px", borderRadius: "6px", overflow: "hidden",
|
||||
display: "grid", border: "1px solid var(--border)", position: "relative", flex: "none"
|
||||
};
|
||||
let bands = [];
|
||||
if (spec.rows) {
|
||||
bands = spec.rows;
|
||||
style.gridTemplateRows = spec.sizes || "repeat(" + bands.length + ", 1fr)";
|
||||
} else if (spec.cols) {
|
||||
bands = spec.cols;
|
||||
style.gridAutoFlow = "column";
|
||||
style.gridTemplateColumns = spec.sizes || "repeat(" + bands.length + ", 1fr)";
|
||||
} else {
|
||||
style.background = spec.bg || "var(--surface-2)";
|
||||
}
|
||||
const kids = bands.map((c, i) => h("span", { key: "b" + i, style: { background: c, display: "block" } }));
|
||||
(spec.marks || []).forEach((m, i) => kids.push(h("span", {
|
||||
key: "m" + i,
|
||||
style: {
|
||||
position: "absolute", left: m.l + "px", top: m.t + "px", width: m.w + "px", height: m.h + "px",
|
||||
borderRadius: m.r || 0, background: m.bg, clipPath: m.clip || "none", display: "block"
|
||||
}
|
||||
})));
|
||||
return h("span", { style: style }, kids);
|
||||
}
|
||||
|
||||
async drawGlobe() {
|
||||
const host = document.getElementById("veilix-globe");
|
||||
if (!host) return;
|
||||
if (!window.d3 || !window.topojson) { setTimeout(() => this.drawGlobe(), 200); return; }
|
||||
const d3 = window.d3;
|
||||
let topo;
|
||||
try {
|
||||
topo = await d3.json("https://cdn.jsdelivr.net/npm/world-atlas@2.0.2/countries-110m.json");
|
||||
} catch (e) { return; }
|
||||
const land = window.topojson.feature(topo, topo.objects.countries);
|
||||
const S = 440, r = S / 2 - 4;
|
||||
const projection = d3.geoOrthographic().rotate([-20, -22]).scale(r).translate([S / 2, S / 2]);
|
||||
const path = d3.geoPath(projection);
|
||||
host.innerHTML = "";
|
||||
const svg = d3.select(host).append("svg")
|
||||
.attr("viewBox", "0 0 " + S + " " + S)
|
||||
.attr("width", "100%").attr("height", "100%")
|
||||
.style("display", "block").style("overflow", "visible");
|
||||
svg.append("circle").attr("cx", S / 2).attr("cy", S / 2).attr("r", r)
|
||||
.attr("fill", "var(--ocean)").attr("stroke", "var(--border)");
|
||||
const graticule = d3.geoGraticule10 ? d3.geoGraticule10() : null;
|
||||
if (graticule) {
|
||||
svg.append("path").datum(graticule).attr("d", path)
|
||||
.attr("fill", "none").attr("stroke", "var(--land-line)")
|
||||
.attr("stroke-width", 0.5).attr("opacity", 0.5);
|
||||
}
|
||||
svg.append("g").selectAll("path").data(land.features).join("path")
|
||||
.attr("d", path).attr("fill", "var(--land)")
|
||||
.attr("stroke", "var(--land-line)").attr("stroke-width", 0.6);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.drawGlobe();
|
||||
const mq = window.matchMedia("(prefers-color-scheme: dark)");
|
||||
this.mq = mq;
|
||||
this.onSys = () => this.setState({ sysDark: mq.matches });
|
||||
mq.addEventListener("change", this.onSys);
|
||||
this.onResize = () => {
|
||||
const n = window.innerWidth < 900;
|
||||
const st = window.innerWidth < 700;
|
||||
if (n !== this.state.narrow) this.setState({ narrow: n, menu: false });
|
||||
if (st !== this.state.stacked) this.setState({ stacked: st });
|
||||
};
|
||||
window.addEventListener("resize", this.onResize);
|
||||
this.onResize();
|
||||
let saved = null;
|
||||
try { saved = localStorage.getItem("veilix.theme"); } catch (e) {}
|
||||
let savedLang = null;
|
||||
try { savedLang = localStorage.getItem("veilix.lang"); } catch (e) {}
|
||||
this.setState({ sysDark: mq.matches, theme: saved || this.props.defaultTheme || "light", lang: savedLang || "ru" }, () => this.applyLang());
|
||||
}
|
||||
|
||||
applyLang() {
|
||||
const lang = this.state.lang || "ru";
|
||||
if (window.VEILIX_APPLY_LANG) window.VEILIX_APPLY_LANG(document.body, lang);
|
||||
else setTimeout(() => this.applyLang(), 150);
|
||||
}
|
||||
|
||||
setLang(l) {
|
||||
try { localStorage.setItem("veilix.lang", l); } catch (e) {}
|
||||
this.setState({ lang: l, langMenu: false }, () => this.applyLang());
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.mq) this.mq.removeEventListener("change", this.onSys);
|
||||
if (this.onResize) window.removeEventListener("resize", this.onResize);
|
||||
}
|
||||
|
||||
componentDidUpdate() { this.apply(); this.applyLang(); }
|
||||
|
||||
apply() {
|
||||
const theme = this.state.theme || this.props.defaultTheme || "light";
|
||||
const dark = theme === "dark" || (theme === "system" && this.state.sysDark);
|
||||
document.documentElement.setAttribute("data-veilix-theme", dark ? "dark" : "light");
|
||||
}
|
||||
|
||||
setTheme(t) {
|
||||
try { localStorage.setItem("veilix.theme", t); } catch (e) {}
|
||||
this.setState({ theme: t }, () => this.apply());
|
||||
}
|
||||
|
||||
renderVals() {
|
||||
const theme = this.state.theme || this.props.defaultTheme || "light";
|
||||
const mark = this.state.mark || this.props.logoMark || "layers";
|
||||
const marks = ["layers", "veil", "arcs", "aperture"];
|
||||
const sel = "var(--sel)";
|
||||
const v = {
|
||||
countries: this.data().map(d => ({ name: d.country })),
|
||||
pickMonth: () => this.setState({ plan: "month" }),
|
||||
pickYear: () => this.setState({ plan: "year" }),
|
||||
planMonthBg: this.state.plan === "month" ? sel : "transparent",
|
||||
planYearBg: this.state.plan === "year" ? sel : "transparent",
|
||||
planMonthBorder: this.state.plan === "month" ? "var(--accent)" : "var(--border)",
|
||||
planYearBorder: this.state.plan === "year" ? "var(--accent)" : "var(--border)",
|
||||
locHint: this.data()[this.state.loc].ready ? "Свободные серверы есть сейчас" : "Сервер установим под вас за 24 часа",
|
||||
openLead: () => this.setState({ lead: true }),
|
||||
sendLead: () => this.setState({ leadSent: true }),
|
||||
leadOpen: this.state.lead,
|
||||
leadClosed: !this.state.lead,
|
||||
leadButton: this.state.leadSent ? "Заявка отправлена" : "Отправить",
|
||||
curSetup: this.data()[this.state.loc].setup,
|
||||
curMonth: this.data()[this.state.loc].month,
|
||||
items: this.data().map((d, n) => ({
|
||||
country: d.country,
|
||||
city: d.city,
|
||||
flag: this.flag(d.code),
|
||||
setup: d.setup,
|
||||
month: d.month,
|
||||
bg: this.state.loc === n ? sel : "transparent",
|
||||
border: this.state.loc === n ? "var(--accent)" : "var(--border)",
|
||||
pick: () => this.setState({ loc: n })
|
||||
})),
|
||||
logoPickerOff: this.props.showLogoPicker !== true,
|
||||
langMenuOff: !this.state.langMenu,
|
||||
heroAlign: this.state.stacked ? "center" : "end",
|
||||
navOff: this.state.narrow,
|
||||
burgerOff: !this.state.narrow,
|
||||
menuOff: !(this.state.narrow && this.state.menu),
|
||||
toggleMenu: () => this.setState(s => ({ menu: !s.menu, langMenu: false })),
|
||||
closeMenu: () => this.setState({ menu: false }),
|
||||
setRu: () => this.setLang("ru"),
|
||||
setEn: () => this.setLang("en"),
|
||||
langLabel: this.state.lang === "en" ? "EN" : "RU",
|
||||
ruBg: this.state.lang === "en" ? "transparent" : sel,
|
||||
enBg: this.state.lang === "en" ? sel : "transparent",
|
||||
ruInk: this.state.lang === "en" ? "var(--ink-3)" : "var(--ink)",
|
||||
enInk: this.state.lang === "en" ? "var(--ink)" : "var(--ink-3)",
|
||||
ruCheck: this.state.lang === "en" ? "" : "✓",
|
||||
enCheck: this.state.lang === "en" ? "✓" : "",
|
||||
toggleLangMenu: () => this.setState(s => ({ langMenu: !s.langMenu })),
|
||||
setLight: () => this.setTheme("light"),
|
||||
setDark: () => this.setTheme("dark"),
|
||||
setSystem: () => this.setTheme("system"),
|
||||
lightBg: theme === "light" ? sel : "transparent",
|
||||
darkBg: theme === "dark" ? sel : "transparent",
|
||||
systemBg: theme === "system" ? sel : "transparent",
|
||||
lightInk: theme === "light" ? "var(--ink)" : "var(--ink-3)",
|
||||
darkInk: theme === "dark" ? "var(--ink)" : "var(--ink-3)",
|
||||
systemInk: theme === "system" ? "var(--ink)" : "var(--ink-3)"
|
||||
};
|
||||
for (let i = 0; i < 5; i++) {
|
||||
v["loc" + i + "Bg"] = this.state.loc === i ? sel : "transparent";
|
||||
v["loc" + i + "Dot"] = this.state.loc === i ? "var(--accent)" : "var(--ink-3)";
|
||||
v["pickLoc" + i] = () => this.setState({ loc: i });
|
||||
}
|
||||
for (let i = 0; i < 6; i++) {
|
||||
v["faq" + i] = () => this.setState(s => ({ faq: s.faq === i ? -1 : i }));
|
||||
v["faq" + i + "Off"] = this.state.faq !== i;
|
||||
v["faq" + i + "Rot"] = this.state.faq === i ? "rotate(-135deg) translateY(2px)" : "rotate(45deg) translateY(-2px)";
|
||||
}
|
||||
marks.forEach((name, idx) => {
|
||||
const n = idx + 1;
|
||||
v["mark" + n + "Off"] = mark !== name;
|
||||
v["mark" + n + "Bg"] = mark === name ? sel : "transparent";
|
||||
v["pickMark" + n] = () => this.setState({ mark: name });
|
||||
});
|
||||
return v;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,296 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<script src="./support.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<x-dc>
|
||||
<helmet>
|
||||
<link rel="icon" type="image/svg+xml" href="favicon-mass.svg" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin="anonymous" />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Commissioner:wght@300;400;500;600&family=Unbounded:wght@500;600&display=swap" rel="stylesheet" />
|
||||
<script>(function(){if(window.__veilixI18nSrc)return;window.__veilixI18nSrc="./i18n.js?v="+Date.now().toString(36)+Math.random().toString(36).slice(2,8);var s=document.createElement("script");s.src=window.__veilixI18nSrc;document.head.appendChild(s);})();</script>
|
||||
<style>
|
||||
:root{
|
||||
--bg: oklch(99.6% 0.0015 215);
|
||||
--surface: oklch(100% 0 0);
|
||||
--surface-2: oklch(98.4% 0.003 215);
|
||||
--ink: oklch(24% 0.04 204);
|
||||
--ink-2: oklch(45% 0.03 204);
|
||||
--ink-3: oklch(60% 0.025 204);
|
||||
--border: oklch(92% 0.014 200);
|
||||
--accent: oklch(48% 0.105 178);
|
||||
--accent-hover: oklch(41% 0.1 178);
|
||||
--accent-soft: oklch(95.5% 0.032 180);
|
||||
--sel: oklch(96.5% 0.016 190);
|
||||
--on-accent: oklch(99% 0.01 180);
|
||||
--glow-1: oklch(86% 0.085 190);
|
||||
--glow-2: oklch(88% 0.075 205);
|
||||
--glow-3: oklch(89% 0.07 172);
|
||||
--veil: oklch(100% 0 0 / 0.55);
|
||||
--shadow: 0 1px 2px oklch(24% 0.04 204 / 0.05), 0 12px 32px -12px oklch(24% 0.04 204 / 0.14);
|
||||
}
|
||||
:root[data-veilix-theme="dark"]{
|
||||
--bg: oklch(23% 0.026 210);
|
||||
--surface: oklch(26.5% 0.028 210);
|
||||
--surface-2: oklch(30% 0.03 210);
|
||||
--ink: oklch(96% 0.012 196);
|
||||
--ink-2: oklch(80% 0.02 196);
|
||||
--ink-3: oklch(65% 0.025 196);
|
||||
--border: oklch(36% 0.04 204);
|
||||
--accent: oklch(76% 0.105 190);
|
||||
--accent-hover: oklch(83% 0.09 190);
|
||||
--accent-soft: oklch(31% 0.05 190);
|
||||
--sel: oklch(30% 0.03 195);
|
||||
--on-accent: oklch(20% 0.04 204);
|
||||
--glow-1: oklch(52% 0.09 200);
|
||||
--glow-2: oklch(47% 0.085 215);
|
||||
--glow-3: oklch(50% 0.08 180);
|
||||
--veil: oklch(100% 0 0 / 0.07);
|
||||
--shadow: 0 1px 2px oklch(12% 0.03 204 / 0.4), 0 16px 40px -16px oklch(9% 0.03 204 / 0.7);
|
||||
}
|
||||
*{ box-sizing: border-box; }
|
||||
input, textarea, select{ font-size: 16px; }
|
||||
[hidden]{ display: none !important; }
|
||||
body{ margin:0; background: var(--bg); color: var(--ink); font-family:"Commissioner", system-ui, sans-serif; -webkit-font-smoothing: antialiased; }
|
||||
a{ color: var(--accent); text-decoration: none; }
|
||||
a:hover{ color: var(--accent-hover); }
|
||||
input::placeholder, textarea::placeholder{ color: var(--ink-3); }
|
||||
</style>
|
||||
</helmet>
|
||||
|
||||
<template id="__bundler_thumbnail" data-bg-color="#e3f4f1">
|
||||
<svg viewBox="0 0 1200 800" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="480" y="290" width="150" height="150" rx="48" fill="#00705e" />
|
||||
<rect x="570" y="360" width="150" height="150" rx="48" fill="#ffffff" fill-opacity="0.62" stroke="#00705e" stroke-width="6" />
|
||||
</svg>
|
||||
</template>
|
||||
<div style="background: var(--bg); color: var(--ink); font-family: 'Commissioner', system-ui, sans-serif; font-weight: 400; letter-spacing: -0.01em; min-height: 100vh;">
|
||||
|
||||
<header style="border-bottom: 1px solid var(--border); background: var(--bg);">
|
||||
<div style="max-width: 1080px; margin: 0 auto; padding: 16px 24px; display: flex; flex-wrap: wrap; align-items: center; gap: 14px 24px;">
|
||||
<a href="MassMain.dc.html" style="display: flex; align-items: center; gap: 10px; color: var(--ink); flex: none;">
|
||||
<span style="position: relative; width: 26px; height: 26px; display: block; flex: none;">
|
||||
<span style="position: absolute; left: 0; top: 3px; width: 19px; height: 19px; border-radius: 6px; background: linear-gradient(140deg, var(--accent), color-mix(in oklab, var(--accent) 55%, var(--glow-3))); display: block;"></span>
|
||||
<span style="position: absolute; right: 0; bottom: 3px; width: 19px; height: 19px; border-radius: 6px; background: var(--veil); border: 1px solid color-mix(in oklab, var(--accent) 35%, transparent); display: block;"></span>
|
||||
</span>
|
||||
<span style="font-family: 'Unbounded', 'Commissioner', sans-serif; font-weight: 600; font-size: 16px; letter-spacing: -0.02em;">ВейликсВПН</span>
|
||||
</a>
|
||||
<div style="margin-left: auto; display: flex; align-items: center; gap: 12px; font-size: 14px; color: var(--ink-3);">
|
||||
<button type="button" onClick="{{ toggleLang }}" title="{{ langTitle }}" style="display: flex; align-items: center; gap: 8px; height: 36px; padding: 0 12px; border-radius: 10px; border: 1px solid var(--border); background: transparent; color: var(--ink-2); font-family: inherit; font-size: 14px; cursor: pointer;" style-hover="color: var(--ink); border-color: var(--ink-3);">
|
||||
<span style="position: relative; width: 16px; height: 16px; border-radius: 50%; border: 1.5px solid currentColor; display: block; flex: none;">
|
||||
<span style="position: absolute; left: -1.5px; top: 5.75px; width: 16px; height: 1.5px; background: currentColor; display: block;"></span>
|
||||
<span style="position: absolute; left: 3.25px; top: -1.5px; width: 8px; height: 16px; border-radius: 50%; border: 1.5px solid currentColor; display: block;"></span>
|
||||
</span>
|
||||
<span style="font-variant-numeric: tabular-nums;">{{ langLabel }}</span>
|
||||
</button>
|
||||
<span>Оформление заказа</span>
|
||||
<a href="MassPrice.dc.html" style="color: var(--ink-2);" style-hover="color: var(--ink);">Отменить</a>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main style="max-width: 1080px; margin: 0 auto; padding: 40px 24px 80px; display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 32px; align-items: start;">
|
||||
|
||||
<div style="display: grid; gap: 16px;">
|
||||
|
||||
<div style="border: 1px solid {{ step1Border }}; border-radius: 20px; background: var(--surface); box-shadow: var(--shadow); overflow: hidden;">
|
||||
<div style="display: flex; align-items: center; gap: 14px; padding: 22px 24px;">
|
||||
<span style="display: flex; align-items: center; justify-content: center; width: 26px; height: 26px; border-radius: 50%; background: {{ step1Dot }}; color: {{ step1DotInk }}; font-size: 13px; font-weight: 500; flex: none;">1</span>
|
||||
<span style="flex: 1; font-size: 18px; font-weight: 500; letter-spacing: -0.02em;">Почта</span>
|
||||
<span hidden="{{ emailNotDone }}" style="font-size: 14px; color: var(--ink-2);">{{ emailValue }}</span>
|
||||
<button type="button" hidden="{{ emailNotDone }}" onClick="{{ editEmail }}" style="border: 0; background: transparent; color: var(--accent); font-family: inherit; font-size: 14px; cursor: pointer; padding: 0;">Изменить</button>
|
||||
</div>
|
||||
|
||||
<div hidden="{{ step1Off }}" style="padding: 0 24px 24px;">
|
||||
<p style="margin: 0 0 16px; max-width: 46em; font-size: 15px; line-height: 1.55; color: var(--ink-2); font-weight: 300;">Введите почту — пришлём одноразовый код. Отдельной регистрации нет: если вы у нас уже были, попадёте в свой кабинет, если нет — он создастся сам.</p>
|
||||
<div hidden="{{ codeSent }}" style="display: flex; flex-wrap: wrap; gap: 10px;">
|
||||
<input type="email" value="{{ emailValue }}" onInput="{{ onEmail }}" placeholder="you@example.com" style="flex: 1; min-width: 220px; height: 50px; padding: 0 14px; border-radius: 12px; border: 1px solid var(--border); background: var(--bg); color: var(--ink); font-family: inherit; font-size: 15px;" />
|
||||
<button type="button" onClick="{{ sendCode }}" style="height: 50px; padding: 0 22px; border-radius: 12px; border: 0; background: var(--accent); color: var(--on-accent); font-family: inherit; font-size: 15px; font-weight: 500; cursor: pointer;" style-hover="background: var(--accent-hover);">Прислать код</button>
|
||||
</div>
|
||||
<div hidden="{{ codeNotSent }}" style="display: grid; gap: 12px;">
|
||||
<div style="font-size: 14px; color: var(--ink-2);">{{ codeLine }}</div>
|
||||
<div style="display: flex; flex-wrap: wrap; gap: 10px;">
|
||||
<input type="text" inputmode="numeric" placeholder="6 цифр" style="flex: 1; min-width: 180px; height: 50px; padding: 0 14px; border-radius: 12px; border: 1px solid var(--border); background: var(--bg); color: var(--ink); font-family: inherit; font-size: 17px; letter-spacing: 0.28em; font-variant-numeric: tabular-nums;" />
|
||||
<button type="button" onClick="{{ confirmCode }}" style="height: 50px; padding: 0 22px; border-radius: 12px; border: 0; background: var(--accent); color: var(--on-accent); font-family: inherit; font-size: 15px; font-weight: 500; cursor: pointer;" style-hover="background: var(--accent-hover);">Продолжить</button>
|
||||
</div>
|
||||
<button type="button" onClick="{{ editEmail }}" style="justify-self: start; border: 0; background: transparent; color: var(--ink-3); font-family: inherit; font-size: 14px; cursor: pointer; padding: 0;">Другая почта</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="border: 1px solid {{ step2Border }}; border-radius: 20px; background: var(--surface); box-shadow: var(--shadow); overflow: hidden;">
|
||||
<div style="display: flex; align-items: center; gap: 14px; padding: 22px 24px;">
|
||||
<span style="display: flex; align-items: center; justify-content: center; width: 26px; height: 26px; border-radius: 50%; background: {{ step2Dot }}; color: {{ step2DotInk }}; font-size: 13px; font-weight: 500; flex: none;">2</span>
|
||||
<span style="flex: 1; font-size: 18px; font-weight: 500; letter-spacing: -0.02em;">Оплата</span>
|
||||
</div>
|
||||
|
||||
<div hidden="{{ step2Off }}" style="padding: 0 24px 24px; display: grid; gap: 16px;">
|
||||
<div style="display: flex; flex-wrap: wrap; gap: 8px;">
|
||||
<button type="button" onClick="{{ payCard }}" style="flex: 1; min-width: 140px; padding: 14px 16px; border-radius: 14px; border: 1px solid {{ cardBorder }}; background: {{ cardBg }}; color: var(--ink); font-family: inherit; font-size: 15px; text-align: left; cursor: pointer;">Карта</button>
|
||||
<button type="button" onClick="{{ payMir }}" style="flex: 1; min-width: 140px; padding: 14px 16px; border-radius: 14px; border: 1px solid {{ mirBorder }}; background: {{ mirBg }}; color: var(--ink); font-family: inherit; font-size: 15px; text-align: left; cursor: pointer;">Карта МИР</button>
|
||||
<button type="button" onClick="{{ payCrypto }}" style="flex: 1; min-width: 140px; padding: 14px 16px; border-radius: 14px; border: 1px solid {{ cryptoBorder }}; background: {{ cryptoBg }}; color: var(--ink); font-family: inherit; font-size: 15px; text-align: left; cursor: pointer;">Криптовалюта</button>
|
||||
</div>
|
||||
|
||||
<div hidden="{{ cryptoChosen }}" style="display: grid; gap: 10px;">
|
||||
<input type="text" placeholder="Номер карты" style="height: 50px; padding: 0 14px; border-radius: 12px; border: 1px solid var(--border); background: var(--bg); color: var(--ink); font-family: inherit; font-size: 15px; width: 100%; font-variant-numeric: tabular-nums;" />
|
||||
<div style="display: flex; flex-wrap: wrap; gap: 10px;">
|
||||
<input type="text" placeholder="ММ / ГГ" style="flex: 1; min-width: 120px; height: 50px; padding: 0 14px; border-radius: 12px; border: 1px solid var(--border); background: var(--bg); color: var(--ink); font-family: inherit; font-size: 15px; font-variant-numeric: tabular-nums;" />
|
||||
<input type="text" placeholder="CVC" style="flex: 1; min-width: 120px; height: 50px; padding: 0 14px; border-radius: 12px; border: 1px solid var(--border); background: var(--bg); color: var(--ink); font-family: inherit; font-size: 15px; font-variant-numeric: tabular-nums;" />
|
||||
</div>
|
||||
<div style="font-size: 13px; color: var(--ink-3); line-height: 1.5;">Карта сохранится для продления. Отменить или заменить её можно в кабинете в любой момент.</div>
|
||||
</div>
|
||||
|
||||
<div hidden="{{ cardChosen }}" style="display: grid; gap: 10px; padding: 18px; border-radius: 14px; background: var(--surface-2); border: 1px solid var(--border);">
|
||||
<div style="font-size: 15px;">USDT, BTC или ETH</div>
|
||||
<div style="font-size: 14px; line-height: 1.55; color: var(--ink-2); font-weight: 300;">После нажатия покажем адрес и сумму. При оплате криптой продление не автоматическое — пришлём напоминание за три дня до конца периода.</div>
|
||||
</div>
|
||||
|
||||
<label style="display: flex; align-items: flex-start; gap: 10px; font-size: 14px; color: var(--ink-2); font-weight: 300; line-height: 1.5; cursor: pointer;">
|
||||
<input type="checkbox" style="margin-top: 3px; width: 16px; height: 16px; accent-color: var(--accent);" />
|
||||
<span>Согласен с условиями использования и политикой конфиденциальности</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<aside style="position: sticky; top: 24px; display: grid; gap: 16px;">
|
||||
<div style="padding: clamp(20px, 4.6vw, 26px); border-radius: 20px; background: var(--surface); border: 1px solid var(--border); box-shadow: var(--shadow);">
|
||||
<div style="font-size: 13px; color: var(--ink-3); margin-bottom: 16px;">Ваш заказ</div>
|
||||
<div style="display: grid; gap: 6px; margin-bottom: 20px;">
|
||||
<div style="font-size: 20px; font-weight: 600; letter-spacing: -0.025em;">Подписка ВейликсВПН</div>
|
||||
<div style="font-size: 15px; color: var(--ink-2);">30 стран и российский адрес</div>
|
||||
<div style="display: inline-flex; align-items: center; gap: 7px; font-size: 13px; color: var(--accent);">
|
||||
<span style="width: 6px; height: 6px; border-radius: 50%; background: var(--accent); display: block;"></span>
|
||||
Доступ открывается сразу после оплаты
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: flex; gap: 8px; margin-bottom: 18px;">
|
||||
<button type="button" onClick="{{ pickMonth }}" style="flex: 1; padding: 12px 14px; border-radius: 12px; border: 1px solid {{ planMonthBorder }}; background: {{ planMonthBg }}; color: var(--ink); font-family: inherit; font-size: 14px; cursor: pointer;">Месяц · 699 ₽</button>
|
||||
<button type="button" onClick="{{ pickYear }}" style="flex: 1; padding: 12px 14px; border-radius: 12px; border: 1px solid {{ planYearBorder }}; background: {{ planYearBg }}; color: var(--ink); font-family: inherit; font-size: 14px; cursor: pointer;">Год · 6 990 ₽</button>
|
||||
</div>
|
||||
<div style="display: grid; gap: 10px; padding-top: 18px; border-top: 1px solid var(--border); font-size: 15px;">
|
||||
<div style="display: flex; justify-content: space-between; gap: 12px; color: var(--ink-2);"><span>Локации</span><span>все 30</span></div>
|
||||
<div style="display: flex; justify-content: space-between; gap: 12px; color: var(--ink-2);"><span>RU-адрес</span><span>включён</span></div>
|
||||
<div style="display: flex; justify-content: space-between; gap: 12px; color: var(--ink-2);"><span>Устройств</span><span>до 5</span></div>
|
||||
</div>
|
||||
<div style="display: flex; justify-content: space-between; align-items: baseline; gap: 12px; margin-top: 18px; padding-top: 18px; border-top: 1px solid var(--border);">
|
||||
<span style="font-size: 16px; font-weight: 500;">К оплате сегодня</span>
|
||||
<span style="font-size: 26px; font-weight: 600; letter-spacing: -0.03em; font-variant-numeric: tabular-nums;">{{ payTotal }}</span>
|
||||
</div>
|
||||
<div style="margin-top: 8px; font-size: 13px; color: var(--ink-3);">{{ nextChargeLine }}</div>
|
||||
<button type="button" onClick="{{ pay }}" disabled="{{ payDisabled }}" style="margin-top: 20px; width: 100%; height: 52px; border-radius: 14px; border: 0; background: {{ payBg }}; color: {{ payInk }}; font-family: inherit; font-size: 16px; font-weight: 500; cursor: {{ payCursor }};">{{ payLabel }}</button>
|
||||
<div hidden="{{ paidOff }}" style="margin-top: 14px; padding: 14px; border-radius: 12px; background: var(--accent-soft); font-size: 14px; line-height: 1.5; color: var(--ink);">Оплачено. Подписка уже в кабинете — <a href="MassApp.dc.html">открыть</a>.</div>
|
||||
</div>
|
||||
</aside>
|
||||
</main>
|
||||
</div>
|
||||
</x-dc>
|
||||
<script type="text/x-dc" data-dc-script data-props="{"defaultTheme":{"editor":"enum","options":["light","dark","system"],"default":"light","tsType":"'light' | 'dark' | 'system'","section":"Тема"}}">
|
||||
class Component extends DCLogic {
|
||||
state = { plan: "year", theme: null, sysDark: false, email: "", sent: false, authed: false, method: "card", paid: false, lang: "ru" };
|
||||
|
||||
componentDidMount() {
|
||||
const mq = window.matchMedia("(prefers-color-scheme: dark)");
|
||||
this.mq = mq;
|
||||
this.onSys = () => this.setState({ sysDark: mq.matches });
|
||||
mq.addEventListener("change", this.onSys);
|
||||
let saved = null;
|
||||
try { saved = localStorage.getItem("veilix.theme"); } catch (e) {}
|
||||
let savedLang = null;
|
||||
try { savedLang = localStorage.getItem("veilix.lang"); } catch (e) {}
|
||||
this.setState({ sysDark: mq.matches, theme: saved || this.props.defaultTheme || "light", lang: savedLang || "ru" }, () => { this.apply(); this.applyLang(); });
|
||||
}
|
||||
|
||||
applyLang() {
|
||||
const lang = this.state.lang || "ru";
|
||||
if (window.VEILIX_APPLY_LANG) window.VEILIX_APPLY_LANG(document.body, lang);
|
||||
else setTimeout(() => this.applyLang(), 150);
|
||||
}
|
||||
|
||||
setLang(l) {
|
||||
try { localStorage.setItem("veilix.lang", l); } catch (e) {}
|
||||
this.setState({ lang: l }, () => this.applyLang());
|
||||
}
|
||||
|
||||
componentWillUnmount() { if (this.mq) this.mq.removeEventListener("change", this.onSys); }
|
||||
componentDidUpdate() { this.apply(); this.applyLang(); }
|
||||
|
||||
apply() {
|
||||
const theme = this.state.theme || this.props.defaultTheme || "light";
|
||||
const dark = theme === "dark" || (theme === "system" && this.state.sysDark);
|
||||
document.documentElement.setAttribute("data-veilix-theme", dark ? "dark" : "light");
|
||||
}
|
||||
|
||||
renderVals() {
|
||||
const s = this.state;
|
||||
const authed = s.authed;
|
||||
const nd = new Date();
|
||||
nd.setMonth(nd.getMonth() + 1);
|
||||
const months = ["января", "февраля", "марта", "апреля", "мая", "июня", "июля", "августа", "сентября", "октября", "ноября", "декабря"];
|
||||
const monthsEn = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
|
||||
const en = s.lang === "en";
|
||||
const sel = "var(--sel)";
|
||||
const yearly = s.plan === "year";
|
||||
const total = yearly ? "6 990 ₽" : "699 ₽";
|
||||
return {
|
||||
pickMonth: () => this.setState({ plan: "month" }),
|
||||
pickYear: () => this.setState({ plan: "year" }),
|
||||
planMonthBg: yearly ? "transparent" : sel,
|
||||
planYearBg: yearly ? sel : "transparent",
|
||||
planMonthBorder: yearly ? "var(--border)" : "var(--accent)",
|
||||
planYearBorder: yearly ? "var(--accent)" : "var(--border)",
|
||||
payTotal: total,
|
||||
emailValue: s.email,
|
||||
toggleLang: () => this.setLang(s.lang === "en" ? "ru" : "en"),
|
||||
langLabel: s.lang === "en" ? "EN" : "RU",
|
||||
langTitle: s.lang === "en" ? "Язык: английский" : "Язык: русский",
|
||||
onEmail: (e) => this.setState({ email: e.target.value }),
|
||||
sendCode: () => this.setState({ sent: true }),
|
||||
confirmCode: () => this.setState({ authed: true }),
|
||||
editEmail: () => this.setState({ sent: false, authed: false }),
|
||||
codeSent: s.sent,
|
||||
codeNotSent: !s.sent,
|
||||
emailNotDone: !authed,
|
||||
step1Off: authed,
|
||||
step1Border: authed ? "var(--border)" : "var(--accent)",
|
||||
step1Dot: authed ? "var(--accent)" : "var(--accent-soft)",
|
||||
step1DotInk: authed ? "var(--on-accent)" : "var(--accent)",
|
||||
step2Off: !authed,
|
||||
step2Border: authed ? "var(--accent)" : "var(--border)",
|
||||
step2Dot: authed ? "var(--accent-soft)" : "var(--surface-2)",
|
||||
step2DotInk: authed ? "var(--accent)" : "var(--ink-3)",
|
||||
payCard: () => this.setState({ method: "card" }),
|
||||
payMir: () => this.setState({ method: "mir" }),
|
||||
payCrypto: () => this.setState({ method: "crypto" }),
|
||||
cardBg: s.method === "card" ? sel : "transparent",
|
||||
mirBg: s.method === "mir" ? sel : "transparent",
|
||||
cryptoBg: s.method === "crypto" ? sel : "transparent",
|
||||
cardBorder: s.method === "card" ? "var(--accent)" : "var(--border)",
|
||||
mirBorder: s.method === "mir" ? "var(--accent)" : "var(--border)",
|
||||
cryptoBorder: s.method === "crypto" ? "var(--accent)" : "var(--border)",
|
||||
cardChosen: s.method !== "crypto",
|
||||
cryptoChosen: s.method === "crypto",
|
||||
nextCharge: "первое списание " + nd.getDate() + " " + months[nd.getMonth()],
|
||||
nextChargeLine: yearly
|
||||
? (en ? "Renews yearly, next charge in 12 months" : "Продление раз в год, следующее списание через 12 месяцев")
|
||||
: (en ? "Renews monthly, next charge on " + nd.getDate() + " " + monthsEn[nd.getMonth()] : "Продление раз в месяц, следующее списание " + nd.getDate() + " " + months[nd.getMonth()]),
|
||||
codeLine: en
|
||||
? "The code has been sent to " + s.email + ". It is valid for 10 minutes."
|
||||
: "Код отправлен на " + s.email + ". Он действует 10 минут.",
|
||||
pay: () => { if (authed) this.setState({ paid: true }); },
|
||||
payDisabled: !authed,
|
||||
payBg: authed ? "var(--accent)" : "var(--surface-2)",
|
||||
payInk: authed ? "var(--on-accent)" : "var(--ink-3)",
|
||||
payCursor: authed ? "pointer" : "not-allowed",
|
||||
payLabel: s.paid ? "Оплачено" : (s.method === "crypto" ? "Показать адрес для оплаты" : "Оплатить " + total),
|
||||
paidOff: !s.paid
|
||||
};
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,493 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<script src="./support.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<x-dc>
|
||||
<helmet>
|
||||
<link rel="icon" type="image/svg+xml" href="favicon-mass.svg" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin="anonymous" />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Commissioner:wght@300;400;500;600&family=Unbounded:wght@500;600&display=swap" rel="stylesheet" />
|
||||
<script>(function(){if(window.__veilixI18nSrc)return;window.__veilixI18nSrc="./i18n.js?v="+Date.now().toString(36)+Math.random().toString(36).slice(2,8);var s=document.createElement("script");s.src=window.__veilixI18nSrc;document.head.appendChild(s);})();</script>
|
||||
<style>
|
||||
:root{
|
||||
--bg: oklch(99.6% 0.0015 215);
|
||||
--surface: oklch(100% 0 0);
|
||||
--surface-2: oklch(98.4% 0.003 215);
|
||||
--ink: oklch(24% 0.04 204);
|
||||
--ink-2: oklch(45% 0.03 204);
|
||||
--ink-3: oklch(60% 0.025 204);
|
||||
--border: oklch(92% 0.014 200);
|
||||
--accent: oklch(48% 0.105 178);
|
||||
--accent-hover: oklch(41% 0.1 178);
|
||||
--accent-soft: oklch(95.5% 0.032 180);
|
||||
--sel: oklch(96.5% 0.016 190);
|
||||
--on-accent: oklch(99% 0.01 180);
|
||||
--glow-1: oklch(86% 0.085 190);
|
||||
--glow-2: oklch(88% 0.075 205);
|
||||
--glow-3: oklch(89% 0.07 172);
|
||||
--veil: oklch(100% 0 0 / 0.55);
|
||||
--shadow: 0 1px 2px oklch(24% 0.04 204 / 0.05), 0 12px 32px -12px oklch(24% 0.04 204 / 0.14);
|
||||
}
|
||||
:root[data-veilix-theme="dark"]{
|
||||
--bg: oklch(23% 0.026 210);
|
||||
--surface: oklch(26.5% 0.028 210);
|
||||
--surface-2: oklch(30% 0.03 210);
|
||||
--ink: oklch(96% 0.012 196);
|
||||
--ink-2: oklch(80% 0.02 196);
|
||||
--ink-3: oklch(65% 0.025 196);
|
||||
--border: oklch(36% 0.04 204);
|
||||
--accent: oklch(76% 0.105 190);
|
||||
--accent-hover: oklch(83% 0.09 190);
|
||||
--accent-soft: oklch(31% 0.05 190);
|
||||
--sel: oklch(30% 0.03 195);
|
||||
--on-accent: oklch(20% 0.04 204);
|
||||
--glow-1: oklch(52% 0.09 200);
|
||||
--glow-2: oklch(47% 0.085 215);
|
||||
--glow-3: oklch(50% 0.08 180);
|
||||
--veil: oklch(100% 0 0 / 0.07);
|
||||
--shadow: 0 1px 2px oklch(12% 0.03 204 / 0.4), 0 16px 40px -16px oklch(9% 0.03 204 / 0.7);
|
||||
}
|
||||
*{ box-sizing: border-box; }
|
||||
input, textarea, select{ font-size: 16px; }
|
||||
[hidden]{ display: none !important; }
|
||||
body{ margin:0; background: var(--bg); color: var(--ink); font-family:"Commissioner", system-ui, sans-serif; -webkit-font-smoothing: antialiased; }
|
||||
a{ color: var(--accent); text-decoration: none; }
|
||||
a:hover{ color: var(--accent-hover); }
|
||||
@keyframes veilDrift{
|
||||
0%{ transform: translate3d(0,0,0) scale(1); }
|
||||
50%{ transform: translate3d(2%,-3%,0) scale(1.06); }
|
||||
100%{ transform: translate3d(0,0,0) scale(1); }
|
||||
}
|
||||
</style>
|
||||
</helmet>
|
||||
|
||||
<template id="__bundler_thumbnail" data-bg-color="#e3f4f1">
|
||||
<svg viewBox="0 0 1200 800" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="480" y="290" width="150" height="150" rx="48" fill="#00705e" />
|
||||
<rect x="570" y="360" width="150" height="150" rx="48" fill="#ffffff" fill-opacity="0.62" stroke="#00705e" stroke-width="6" />
|
||||
</svg>
|
||||
</template>
|
||||
<div style="background: var(--bg); color: var(--ink); font-family: 'Commissioner', system-ui, sans-serif; font-weight: 400; letter-spacing: -0.01em; min-height: 100vh;">
|
||||
|
||||
<header style="position: sticky; top: 0; z-index: 40; background: color-mix(in oklab, var(--bg) 82%, transparent); backdrop-filter: blur(20px) saturate(1.4); -webkit-backdrop-filter: blur(20px) saturate(1.4); border-bottom: 1px solid var(--border);">
|
||||
<div style="max-width: 1200px; margin: 0 auto; padding: 14px 24px; min-height: 66px; display: flex; flex-wrap: nowrap; align-items: center; gap: 14px 40px;">
|
||||
<a href="MassMain.dc.html" style="display: flex; align-items: center; gap: 10px; color: var(--ink); flex: none;">
|
||||
<span style="position: relative; width: 28px; height: 28px; display: block; flex: none;">
|
||||
<span style="position: absolute; left: 0; top: 3px; width: 21px; height: 21px; border-radius: 7px; background: linear-gradient(140deg, var(--accent), color-mix(in oklab, var(--accent) 55%, var(--glow-3))); display: block;"></span>
|
||||
<span style="position: absolute; right: 0; bottom: 3px; width: 21px; height: 21px; border-radius: 7px; background: var(--veil); border: 1px solid color-mix(in oklab, var(--accent) 35%, transparent); backdrop-filter: blur(2px); display: block;"></span>
|
||||
</span>
|
||||
<span style="font-family: 'Unbounded', 'Commissioner', sans-serif; font-weight: 600; font-size: 17px; letter-spacing: -0.02em; width: 132px; flex: none; white-space: nowrap;">ВейликсВПН</span>
|
||||
</a>
|
||||
<nav hidden="{{ navOff }}" style="display: flex; flex-wrap: wrap; gap: 10px 28px; font-size: 15px; font-weight: 500; color: var(--ink-2);">
|
||||
<a href="#top" style="color: var(--ink);">Цены</a>
|
||||
<a href="MassLocations.dc.html" style="color: var(--ink-2);" style-hover="color: var(--ink);">Локации</a>
|
||||
<a href="MassSupport.dc.html" style="color: var(--ink-2);" style-hover="color: var(--ink);">Поддержка</a>
|
||||
</nav>
|
||||
<div style="margin-left: auto; display: flex; flex-wrap: wrap; align-items: center; gap: 8px;">
|
||||
<div hidden="{{ navOff }}" style="position: relative;">
|
||||
<button type="button" onClick="{{ toggleLangMenu }}" title="Язык" style="display: flex; align-items: center; gap: 7px; height: 38px; padding: 0 11px; border: 1px solid var(--border); border-radius: 10px; background: transparent; color: var(--ink-2); font-family: inherit; font-size: 14px; cursor: pointer;" style-hover="color: var(--ink); border-color: var(--ink-3);">
|
||||
<span style="position: relative; width: 16px; height: 16px; border-radius: 50%; border: 1.5px solid currentColor; display: block; flex: none;">
|
||||
<span style="position: absolute; left: -1.5px; top: 5.75px; width: 16px; height: 1.5px; background: currentColor; display: block;"></span>
|
||||
<span style="position: absolute; left: 3.25px; top: -1.5px; width: 8px; height: 16px; border-radius: 50%; border: 1.5px solid currentColor; display: block;"></span>
|
||||
</span>
|
||||
<span style="font-variant-numeric: tabular-nums;">{{ langLabel }}</span>
|
||||
</button>
|
||||
<div hidden="{{ langMenuOff }}" style="position: absolute; right: 0; top: 46px; width: 172px; padding: 6px; background: var(--surface); border: 1px solid var(--border); border-radius: 14px; box-shadow: var(--shadow); font-size: 14px;">
|
||||
<button type="button" onClick="{{ setRu }}" style="display: flex; align-items: center; justify-content: space-between; gap: 12px; width: 100%; padding: 9px 10px; border: 0; border-radius: 9px; background: {{ ruBg }}; color: var(--ink); font-family: inherit; font-size: 14px; text-align: left; cursor: pointer;">
|
||||
<span>Русский</span>
|
||||
<span style="color: var(--accent);">{{ ruCheck }}</span>
|
||||
</button>
|
||||
<button type="button" onClick="{{ setEn }}" style="display: flex; align-items: center; justify-content: space-between; gap: 12px; width: 100%; padding: 9px 10px; border: 0; border-radius: 9px; background: {{ enBg }}; color: var(--ink); font-family: inherit; font-size: 14px; text-align: left; cursor: pointer;">
|
||||
<span>English</span>
|
||||
<span style="color: var(--accent);">{{ enCheck }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<a href="MassApp.dc.html" hidden="{{ navOff }}" style="display: flex; align-items: center; justify-content: center; height: 38px; width: 78px; flex: none; box-sizing: border-box; border-radius: 10px; color: var(--ink-2); font-size: 15px;" style-hover="color: var(--ink); background: var(--surface-2);">Войти</a>
|
||||
<a href="#locations" hidden="{{ navOff }}" style="display: flex; align-items: center; justify-content: center; height: 38px; width: 142px; flex: none; box-sizing: border-box; border-radius: 10px; background: var(--accent); color: var(--on-accent); font-size: 15px; font-weight: 500; box-shadow: 0 6px 18px -8px var(--accent);" style-hover="background: var(--accent-hover); color: var(--on-accent);">Получить VPN</a>
|
||||
<button type="button" hidden="{{ burgerOff }}" onClick="{{ toggleMenu }}" title="Меню" style="display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 5px; width: 42px; height: 38px; border: 1px solid var(--border); border-radius: 10px; background: transparent; color: var(--ink); cursor: pointer;" style-hover="border-color: var(--ink-3);">
|
||||
<span style="width: 18px; height: 2px; border-radius: 2px; background: currentColor; display: block;"></span>
|
||||
<span style="width: 18px; height: 2px; border-radius: 2px; background: currentColor; display: block;"></span>
|
||||
<span style="width: 18px; height: 2px; border-radius: 2px; background: currentColor; display: block;"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div hidden="{{ menuOff }}" style="position: absolute; left: 0; right: 0; top: 100%; z-index: 45; padding: 10px 20px 18px; background: var(--bg); border-bottom: 1px solid var(--border); box-shadow: var(--shadow); display: grid; gap: 2px;">
|
||||
<a href="MassPrice.dc.html" onClick="{{ closeMenu }}" style="padding: 13px 6px; font-size: 17px; color: var(--ink);">Цены</a>
|
||||
<a href="MassLocations.dc.html" onClick="{{ closeMenu }}" style="padding: 13px 6px; font-size: 17px; color: var(--ink);">Локации</a>
|
||||
<a href="MassSupport.dc.html" onClick="{{ closeMenu }}" style="padding: 13px 6px; font-size: 17px; color: var(--ink);">Поддержка</a>
|
||||
<div style="display: flex; align-items: center; justify-content: space-between; gap: 12px; padding-top: 6px; margin-top: 6px; border-top: 1px solid var(--border);">
|
||||
<a href="MassApp.dc.html" onClick="{{ closeMenu }}" style="padding: 13px 6px; font-size: 17px; color: var(--ink);">Войти</a>
|
||||
<div style="display: flex; gap: 6px; flex: none;">
|
||||
<button type="button" onClick="{{ setRu }}" style="padding: 9px 13px; border: 1px solid var(--border); border-radius: 10px; background: {{ ruBg }}; color: var(--ink); font-family: inherit; font-size: 13px; cursor: pointer; white-space: nowrap;">Русский</button>
|
||||
<button type="button" onClick="{{ setEn }}" style="padding: 9px 13px; border: 1px solid var(--border); border-radius: 10px; background: {{ enBg }}; color: var(--ink); font-family: inherit; font-size: 13px; cursor: pointer; white-space: nowrap;">English</button>
|
||||
</div>
|
||||
</div>
|
||||
<a href="#locations" onClick="{{ closeMenu }}" style="display: flex; align-items: center; justify-content: center; height: 50px; margin-top: 12px; border-radius: 12px; background: var(--accent); color: var(--on-accent); font-size: 16px; font-weight: 500;">Получить VPN</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main id="top" style="max-width: 1200px; margin: 0 auto; padding: 0 24px 80px;">
|
||||
|
||||
<section style="position: relative; overflow: hidden; isolation: isolate; -webkit-transform: translateZ(0); transform: translateZ(0); margin-top: 40px; border-radius: 28px; border: 1px solid var(--border); background: var(--surface); box-shadow: var(--shadow);">
|
||||
<div style="position: absolute; right: min(-80px, calc(-140px + 11vw)); top: min(-120px, calc(-180px + 11vw)); width: clamp(180px, 38vw, 420px); height: clamp(180px, 38vw, 420px); border-radius: 50%; background: radial-gradient(circle at 50% 50%, var(--glow-1) 0%, color-mix(in oklab, var(--glow-1) 35%, transparent) 55%, transparent 78%); filter: blur(70px); opacity: 0.5; animation: veilDrift 18s ease-in-out infinite;"></div>
|
||||
<div style="position: absolute; left: min(-60px, calc(-120px + 11vw)); bottom: min(-140px, calc(-200px + 11vw)); width: clamp(150px, 32vw, 360px); height: clamp(150px, 32vw, 360px); border-radius: 50%; background: radial-gradient(circle at 50% 50%, var(--glow-3) 0%, color-mix(in oklab, var(--glow-3) 35%, transparent) 55%, transparent 78%); filter: blur(70px); opacity: 0.4;"></div>
|
||||
<div style="position: relative; display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: clamp(28px, 5vw, 48px); padding: clamp(28px, 6vw, 56px) clamp(20px, 5vw, 48px);">
|
||||
<div>
|
||||
<div style="display: flex; flex-wrap: wrap; align-items: baseline; gap: 12px;">
|
||||
<span style="font-size: clamp(56px, 8vw, 96px); font-weight: 600; letter-spacing: -0.045em; line-height: 0.95; font-variant-numeric: tabular-nums;">699 ₽</span>
|
||||
<span style="font-size: 18px; color: var(--ink-2);">в месяц</span>
|
||||
</div>
|
||||
<div style="margin-top: 16px; font-size: 20px; color: var(--ink-2); font-variant-numeric: tabular-nums;">или 6 990 ₽ в год — 582 ₽ в месяц</div>
|
||||
<p style="margin: 20px 0 0; max-width: 30em; font-size: 15px; line-height: 1.55; color: var(--ink-3); font-weight: 300;">Одна цена за всё. Отменить продление можно в кабинете.</p>
|
||||
<a href="MassOrder.dc.html" style="display: inline-flex; align-items: center; justify-content: center; height: 50px; margin-top: 24px; padding: 0 24px; border-radius: 12px; background: var(--accent); color: var(--on-accent); font-size: 16px; font-weight: 500; box-shadow: 0 10px 28px -12px var(--accent);" style-hover="background: var(--accent-hover); color: var(--on-accent);">Оформить подписку</a>
|
||||
</div>
|
||||
<div style="display: grid; gap: 14px; align-content: start;">
|
||||
<div style="font-size: 15px; font-weight: 500;">Что входит</div>
|
||||
<div style="display: grid; gap: 12px; font-size: 15px; color: var(--ink-2); font-weight: 300;">
|
||||
<div style="display: flex; gap: 12px;"><span style="color: var(--accent);">—</span><span>Все 30 стран, переключение в любой момент</span></div>
|
||||
<div style="display: flex; gap: 12px;"><span style="color: var(--accent);">—</span><span>Российская локация: прямое соединение и мост в обе стороны</span></div>
|
||||
<div style="display: flex; gap: 12px;"><span style="color: var(--accent);">—</span><span>До 5 устройств одновременно</span></div>
|
||||
<div style="display: flex; gap: 12px;"><span style="color: var(--accent);">—</span><span>Современный протокол XRAY</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="locations" style="padding: 72px 0 0;">
|
||||
<div style="display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; gap: 16px; margin-bottom: 8px;">
|
||||
<h2 style="margin: 0; font-size: clamp(28px, 3vw, 38px); font-weight: 600; letter-spacing: -0.03em;">Страны в подписке</h2>
|
||||
</div>
|
||||
<p style="margin: 0 0 28px; max-width: 52em; font-size: 16px; line-height: 1.55; color: var(--ink-2); font-weight: 300;">Все локации доступны сразу после оплаты и входят в одну цену.</p>
|
||||
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 12px;">
|
||||
<sc-for list="{{ items }}" as="loc" hint-placeholder-count="12">
|
||||
<div role="button" tabindex="0" onClick="{{ loc.pick }}" style="display: grid; align-content: start; gap: 14px; padding: 22px; border-radius: 22px; border: 1px solid {{ loc.border }}; background: {{ loc.bg }}; color: var(--ink); cursor: pointer;" style-hover="border-color: var(--ink-3);">
|
||||
<span style="display: flex; align-items: center; justify-content: space-between; gap: 14px;">
|
||||
{{ loc.flag }}
|
||||
<span style="display: flex; align-items: center; gap: 8px; flex: none; min-height: 32px; font-size: 13px; color: {{ loc.statusInk }};">
|
||||
<span style="width: 6px; height: 6px; border-radius: 50%; background: {{ loc.statusDot }}; border: {{ loc.statusRing }}; display: block;"></span>
|
||||
{{ loc.badge }}
|
||||
</span>
|
||||
</span>
|
||||
<span style="display: grid; gap: 5px;">
|
||||
<span style="font-size: 19px; font-weight: 500; letter-spacing: -0.02em;">{{ loc.country }}</span>
|
||||
<span style="font-size: 14px; color: var(--ink-3);">{{ loc.city }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</sc-for>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
|
||||
<section style="padding: 72px 0 0;">
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 16px;">
|
||||
<div style="padding: clamp(16px, 4vw, 32px); border-radius: 22px; background: var(--surface); border: 1px solid var(--border); box-shadow: var(--shadow);">
|
||||
<h3 style="margin: 0 0 10px; font-size: 22px; font-weight: 600; letter-spacing: -0.025em;">Нужен адрес, закреплённый только за вами</h3>
|
||||
<p style="margin: 0 0 20px; font-size: 15px; line-height: 1.55; color: var(--ink-2); font-weight: 300;">Здесь сервер общий: он дешевле и этого достаточно большинству. Если сервисы требуют постоянный IP, посмотрите индивидуальные тарифы Вейликс.</p>
|
||||
<a href="Main.dc.html" style="display: inline-flex; align-items: center; height: 48px; padding: 0 22px; border-radius: 12px; border: 1px solid var(--accent); color: var(--accent); font-size: 15px; font-weight: 500;" style-hover="background: var(--accent-soft); color: var(--accent);">Индивидуальный IP</a>
|
||||
</div>
|
||||
<div style="position: relative; isolation: isolate; padding: clamp(16px, 4vw, 32px); border-radius: 22px; background: var(--surface); border: 1px solid var(--border); box-shadow: var(--shadow);">
|
||||
<div style="position: absolute; inset: 0; border-radius: 22px; overflow: hidden; pointer-events: none; -webkit-mask-image: -webkit-radial-gradient(white, black); mask-image: radial-gradient(white, black);">
|
||||
<div style="position: absolute; right: -70px; bottom: -90px; width: 260px; height: 260px; border-radius: 50%; background: var(--glow-2); filter: blur(60px); opacity: 0.5;"></div>
|
||||
</div>
|
||||
<div style="position: relative;">
|
||||
<h3 id="lead" style="margin: 0 0 10px; font-size: 22px; font-weight: 600; letter-spacing: -0.025em; scroll-margin-top: 90px;">Нужны особые условия</h3>
|
||||
<p style="margin: 0 0 20px; font-size: 15px; line-height: 1.55; color: var(--ink-2); font-weight: 300;">Подписки на команду, оплата по счёту или редкая страна. Оставьте заявку — ответим в течение дня.</p>
|
||||
<button type="button" onClick="{{ openLead }}" hidden="{{ leadOpen }}" style="display: inline-flex; align-items: center; height: 48px; padding: 0 22px; border-radius: 12px; border: 1px solid var(--accent); background: transparent; color: var(--accent); font-family: inherit; font-size: 15px; font-weight: 500; cursor: pointer;" style-hover="background: var(--accent-soft);">Оставить заявку</button>
|
||||
<div hidden="{{ leadClosed }}" style="display: grid; gap: 10px; max-width: 420px;">
|
||||
<input type="text" placeholder="Имя" style="height: 48px; padding: 0 14px; border-radius: 12px; border: 1px solid var(--border); background: var(--bg); color: var(--ink); font-family: inherit; font-size: 15px; width: 100%;" />
|
||||
<input type="email" placeholder="Email" style="height: 48px; padding: 0 14px; border-radius: 12px; border: 1px solid var(--border); background: var(--bg); color: var(--ink); font-family: inherit; font-size: 15px; width: 100%;" />
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 10px;">
|
||||
<input type="text" placeholder="Telegram" style="height: 48px; padding: 0 14px; border-radius: 12px; border: 1px solid var(--border); background: var(--bg); color: var(--ink); font-family: inherit; font-size: 15px; width: 100%;" />
|
||||
<input type="text" placeholder="WhatsApp" style="height: 48px; padding: 0 14px; border-radius: 12px; border: 1px solid var(--border); background: var(--bg); color: var(--ink); font-family: inherit; font-size: 15px; width: 100%;" />
|
||||
</div>
|
||||
<textarea rows="3" placeholder="Коротко о задаче" style="padding: 12px 14px; border-radius: 12px; border: 1px solid var(--border); background: var(--bg); color: var(--ink); font-family: inherit; font-size: 15px; resize: vertical; width: 100%;"></textarea>
|
||||
<button type="button" onClick="{{ sendLead }}" style="display: inline-flex; align-items: center; justify-content: center; height: 48px; padding: 0 22px; border-radius: 12px; border: 0; background: var(--accent); color: var(--on-accent); font-family: inherit; font-size: 15px; font-weight: 500; cursor: pointer;" style-hover="background: var(--accent-hover);">{{ leadButton }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer style="border-top: 1px solid var(--border); background: var(--surface-2);">
|
||||
<div style="max-width: 1200px; margin: 0 auto; padding: 48px clamp(16px, 4vw, 24px) 32px; display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 32px;">
|
||||
<div>
|
||||
<div style="display: flex; align-items: center; gap: 10px; margin-bottom: 14px;">
|
||||
<span style="position: relative; width: 24px; height: 24px; display: block; flex: none;">
|
||||
<span style="position: absolute; left: 0; top: 3px; width: 18px; height: 18px; border-radius: 6px; background: linear-gradient(140deg, var(--accent), color-mix(in oklab, var(--accent) 55%, var(--glow-3))); display: block;"></span>
|
||||
<span style="position: absolute; right: 0; bottom: 3px; width: 18px; height: 18px; border-radius: 6px; background: var(--veil); border: 1px solid color-mix(in oklab, var(--accent) 35%, transparent); display: block;"></span>
|
||||
</span>
|
||||
<span style="font-family: 'Unbounded', 'Commissioner', sans-serif; font-weight: 600; font-size: 15px; letter-spacing: -0.02em;">ВейликсВПН</span>
|
||||
</div>
|
||||
<p style="margin: 0 0 20px; max-width: 26em; font-size: 14px; line-height: 1.55; color: var(--ink-3); font-weight: 300;">VPN, который не нужно отключать. Работаем с 2022 года.</p>
|
||||
<div style="display: flex; flex-wrap: wrap; gap: 6px;">
|
||||
<span style="display: flex; align-items: center; justify-content: center; width: 60px; height: 28px; border-radius: 8px; border: 1px solid var(--border); background: var(--surface); font-size: 11px; letter-spacing: 0.06em; color: var(--ink-3);">VISA</span>
|
||||
<span style="display: flex; align-items: center; justify-content: center; width: 60px; height: 28px; border-radius: 8px; border: 1px solid var(--border); background: var(--surface); font-size: 11px; letter-spacing: 0.06em; color: var(--ink-3);">MC</span>
|
||||
<span style="display: flex; align-items: center; justify-content: center; width: 60px; height: 28px; border-radius: 8px; border: 1px solid var(--border); background: var(--surface); font-size: 11px; letter-spacing: 0.06em; color: var(--ink-3);">МИР</span>
|
||||
<span style="display: flex; align-items: center; justify-content: center; width: 60px; height: 28px; border-radius: 8px; border: 1px solid var(--border); background: var(--surface); font-size: 11px; letter-spacing: 0.06em; color: var(--ink-3);">CRYPTO</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: grid; grid-template-columns: repeat(2, minmax(130px, 1fr)); gap: 24px 32px; align-content: start;">
|
||||
<div style="display: grid; grid-template-columns: minmax(0, 1fr); gap: 10px; align-content: start; font-size: 14px;">
|
||||
<div style="font-size: 13px; color: var(--ink-3); margin-bottom: 2px;">Сервис</div>
|
||||
<a href="#top" style="color: var(--ink-2);" style-hover="color: var(--ink);">Цены</a>
|
||||
<a href="MassLocations.dc.html" style="color: var(--ink-2);" style-hover="color: var(--ink);">Локации</a>
|
||||
<a href="MassSupport.dc.html" style="color: var(--ink-2);" style-hover="color: var(--ink);">Поддержка</a>
|
||||
</div>
|
||||
<div style="display: grid; grid-template-columns: minmax(0, 1fr); gap: 10px; align-content: start; font-size: 14px;">
|
||||
<div style="font-size: 13px; color: var(--ink-3); margin-bottom: 2px;">Документы</div>
|
||||
<a href="#top" style="color: var(--ink-2);" style-hover="color: var(--ink);">Условия использования</a>
|
||||
<a href="#top" style="color: var(--ink-2);" style-hover="color: var(--ink);">Политика конфиденциальности</a>
|
||||
<a href="#top" style="color: var(--ink-2);" style-hover="color: var(--ink);">Возврат средств</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="max-width: 1200px; margin: 0 auto; padding: 0 clamp(16px, 4vw, 24px) 40px; display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 14px;">
|
||||
<div style="font-size: 13px; color: var(--ink-3);">© 2026 ВейликсВПН</div>
|
||||
<div style="display: flex; align-items: center; gap: 8px;">
|
||||
<div style="display: flex; padding: 2px; border-radius: 9px; border: 1px solid var(--border); background: var(--surface); flex: none;">
|
||||
<button type="button" onClick="{{ setLight }}" title="Светлая тема" style="display: flex; align-items: center; justify-content: center; width: 28px; height: 28px; border: 0; border-radius: 7px; background: {{ lightBg }}; color: {{ lightInk }}; cursor: pointer;"><span style="position: relative; width: 14px; height: 14px; display: block;"><span style="position: absolute; left: 3px; top: 3px; width: 8px; height: 8px; border-radius: 50%; background: currentColor; display: block;"></span><span style="position: absolute; left: 0; top: 0; width: 14px; height: 14px; border-radius: 50%; border: 1.5px solid currentColor; opacity: 0.45; display: block;"></span></span></button>
|
||||
<button type="button" onClick="{{ setDark }}" title="Тёмная тема" style="display: flex; align-items: center; justify-content: center; width: 28px; height: 28px; border: 0; border-radius: 7px; background: {{ darkBg }}; color: {{ darkInk }}; cursor: pointer;"><span style="width: 14px; height: 14px; border-radius: 50%; box-shadow: inset -5px -1.5px 0 0 currentColor; display: block;"></span></button>
|
||||
<button type="button" onClick="{{ setSystem }}" title="Как в системе" style="display: flex; align-items: center; justify-content: center; width: 28px; height: 28px; border: 0; border-radius: 7px; background: {{ systemBg }}; color: {{ systemInk }}; cursor: pointer;"><span style="width: 14px; height: 14px; border-radius: 50%; border: 1.5px solid currentColor; background: linear-gradient(90deg, currentColor 50%, transparent 50%); display: block;"></span></button>
|
||||
</div>
|
||||
<div style="display: flex; padding: 2px; border-radius: 9px; border: 1px solid var(--border); background: var(--surface); flex: none;">
|
||||
<button type="button" onClick="{{ setRu }}" style="padding: 0 10px; height: 28px; border: 0; border-radius: 7px; background: {{ ruBg }}; color: {{ ruInk }}; font-family: inherit; font-size: 12px; cursor: pointer;">RU</button>
|
||||
<button type="button" onClick="{{ setEn }}" style="padding: 0 10px; height: 28px; border: 0; border-radius: 7px; background: {{ enBg }}; color: {{ enInk }}; font-family: inherit; font-size: 12px; cursor: pointer;">EN</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</x-dc>
|
||||
<script type="text/x-dc" data-dc-script data-props="{"defaultTheme":{"editor":"enum","options":["light","dark","system"],"default":"light","tsType":"'light' | 'dark' | 'system'","section":"Тема"}}">
|
||||
class Component extends DCLogic {
|
||||
state = { theme: null, sysDark: false, loc: 0, lead: false, leadSent: false, lang: "ru", narrow: false, menu: false, langMenu: false };
|
||||
|
||||
data() {
|
||||
const base = { setup: "4 900 ₽", month: "2 499 ₽" };
|
||||
const rare = { setup: "6 900 ₽", month: "3 499 ₽" };
|
||||
const far = { setup: "9 900 ₽", month: "4 999 ₽" };
|
||||
const L = (country, city, code, lng, lat, ready, tier) => ({ country, city, code, lng, lat, ready, setup: tier.setup, month: tier.month, rare: tier !== base });
|
||||
return [
|
||||
L("Нидерланды", "Амстердам", "nl", 4.9, 52.37, true, base),
|
||||
L("Германия", "Франкфурт", "de", 8.68, 50.11, true, base),
|
||||
L("США", "Нью-Йорк", "us", -74.01, 40.71, true, base),
|
||||
L("Великобритания", "Лондон", "gb", -0.13, 51.51, true, base),
|
||||
L("Франция", "Париж", "fr", 2.35, 48.86, true, base),
|
||||
L("Финляндия", "Хельсинки", "fi", 24.94, 60.17, true, base),
|
||||
L("Швеция", "Стокгольм", "se", 18.07, 59.33, true, base),
|
||||
L("Норвегия", "Осло", "no", 10.75, 59.91, false, base),
|
||||
L("Дания", "Копенгаген", "dk", 12.57, 55.68, false, base),
|
||||
L("Швейцария", "Цюрих", "ch", 8.54, 47.37, true, rare),
|
||||
L("Австрия", "Вена", "at", 16.37, 48.21, true, base),
|
||||
L("Испания", "Мадрид", "es", -3.7, 40.42, true, base),
|
||||
L("Италия", "Милан", "it", 9.19, 45.46, true, base),
|
||||
L("Польша", "Варшава", "pl", 21.01, 52.23, true, base),
|
||||
L("Чехия", "Прага", "cz", 14.44, 50.08, true, base),
|
||||
L("Румыния", "Бухарест", "ro", 26.1, 44.43, false, base),
|
||||
L("Литва", "Вильнюс", "lt", 25.28, 54.69, true, base),
|
||||
L("Латвия", "Рига", "lv", 24.11, 56.95, true, base),
|
||||
L("Эстония", "Таллин", "ee", 24.75, 59.44, false, base),
|
||||
L("Турция", "Стамбул", "tr", 28.98, 41.01, true, base),
|
||||
L("Казахстан", "Алматы", "kz", 76.89, 43.24, false, base),
|
||||
L("Армения", "Ереван", "am", 44.51, 40.18, false, base),
|
||||
L("Грузия", "Тбилиси", "ge", 44.8, 41.72, false, base),
|
||||
L("ОАЭ", "Дубай", "ae", 55.27, 25.2, true, rare),
|
||||
L("Сингапур", "Сингапур", "sg", 103.82, 1.35, true, rare),
|
||||
L("Япония", "Токио", "jp", 139.69, 35.69, true, rare),
|
||||
L("Южная Корея", "Сеул", "kr", 126.98, 37.57, false, rare),
|
||||
L("Канада", "Торонто", "ca", -79.38, 43.65, true, base),
|
||||
L("Австралия", "Сидней", "au", 151.21, -33.87, false, far),
|
||||
L("Бразилия", "Сан-Паулу", "br", -46.63, -23.55, false, far)
|
||||
];
|
||||
}
|
||||
|
||||
flagSpecs() {
|
||||
return {
|
||||
nl: { rows: ["#AE1C28", "#FFFFFF", "#21468B"] },
|
||||
de: { rows: ["#000000", "#DD0000", "#FFCE00"] },
|
||||
us: { rows: ["#B22234", "#FFFFFF", "#B22234", "#FFFFFF", "#B22234", "#FFFFFF"], marks: [{ l: 0, t: 0, w: 22, h: 17, bg: "#3C3B6E" }] },
|
||||
gb: { bg: "#012169", marks: [{ l: 17, t: 0, w: 12, h: 32, bg: "#FFFFFF" }, { l: 0, t: 10, w: 46, h: 12, bg: "#FFFFFF" }, { l: 20, t: 0, w: 6, h: 32, bg: "#C8102E" }, { l: 0, t: 13, w: 46, h: 6, bg: "#C8102E" }] },
|
||||
fr: { cols: ["#002395", "#FFFFFF", "#ED2939"] },
|
||||
fi: { bg: "#FFFFFF", marks: [{ l: 13, t: 0, w: 8, h: 32, bg: "#003580" }, { l: 0, t: 12, w: 46, h: 8, bg: "#003580" }] },
|
||||
se: { bg: "#006AA7", marks: [{ l: 13, t: 0, w: 6, h: 32, bg: "#FECC00" }, { l: 0, t: 13, w: 46, h: 6, bg: "#FECC00" }] },
|
||||
no: { bg: "#BA0C2F", marks: [{ l: 12, t: 0, w: 10, h: 32, bg: "#FFFFFF" }, { l: 0, t: 11, w: 46, h: 10, bg: "#FFFFFF" }, { l: 15, t: 0, w: 4, h: 32, bg: "#00205B" }, { l: 0, t: 14, w: 46, h: 4, bg: "#00205B" }] },
|
||||
dk: { bg: "#C8102E", marks: [{ l: 13, t: 0, w: 6, h: 32, bg: "#FFFFFF" }, { l: 0, t: 13, w: 46, h: 6, bg: "#FFFFFF" }] },
|
||||
ch: { bg: "#D52B1E", marks: [{ l: 20, t: 8, w: 6, h: 16, bg: "#FFFFFF" }, { l: 15, t: 13, w: 16, h: 6, bg: "#FFFFFF" }] },
|
||||
at: { rows: ["#ED2939", "#FFFFFF", "#ED2939"] },
|
||||
es: { rows: ["#AA151B", "#F1BF00", "#AA151B"], sizes: "1fr 2fr 1fr" },
|
||||
it: { cols: ["#008C45", "#F4F5F0", "#CD212A"] },
|
||||
pl: { rows: ["#FFFFFF", "#DC143C"] },
|
||||
cz: { rows: ["#FFFFFF", "#D7141A"], marks: [{ l: 0, t: 0, w: 18, h: 32, bg: "#11457E", clip: "polygon(0 0, 100% 50%, 0 100%)" }] },
|
||||
ro: { cols: ["#002B7F", "#FCD116", "#CE1126"] },
|
||||
lt: { rows: ["#FDB913", "#006A44", "#C1272D"] },
|
||||
lv: { rows: ["#9E3039", "#FFFFFF", "#9E3039"], sizes: "2fr 1fr 2fr" },
|
||||
ee: { rows: ["#0072CE", "#000000", "#FFFFFF"] },
|
||||
tr: { bg: "#E30A17", marks: [{ l: 11, t: 8, w: 16, h: 16, r: "50%", bg: "#FFFFFF" }, { l: 16, t: 8, w: 16, h: 16, r: "50%", bg: "#E30A17" }] },
|
||||
kz: { bg: "#00AFCA", marks: [{ l: 16, t: 8, w: 15, h: 15, r: "50%", bg: "#FEC50C" }] },
|
||||
am: { rows: ["#D90012", "#0033A0", "#F2A800"] },
|
||||
ge: { bg: "#FFFFFF", marks: [{ l: 20, t: 0, w: 6, h: 32, bg: "#FF0000" }, { l: 0, t: 13, w: 46, h: 6, bg: "#FF0000" }] },
|
||||
ae: { rows: ["#00732F", "#FFFFFF", "#000000"], marks: [{ l: 0, t: 0, w: 11, h: 32, bg: "#FF0000" }] },
|
||||
sg: { rows: ["#EF3340", "#FFFFFF"], marks: [{ l: 5, t: 3, w: 11, h: 11, r: "50%", bg: "#FFFFFF" }, { l: 9, t: 3, w: 11, h: 11, r: "50%", bg: "#EF3340" }] },
|
||||
jp: { bg: "#FFFFFF", marks: [{ l: 16, t: 9, w: 14, h: 14, r: "50%", bg: "#BC002D" }] },
|
||||
kr: { bg: "#FFFFFF", marks: [{ l: 16, t: 9, w: 14, h: 14, r: "50%", bg: "#CD2E3A" }, { l: 16, t: 9, w: 14, h: 14, r: "50%", bg: "#0047A0", clip: "polygon(0 50%, 100% 50%, 100% 100%, 0 100%)" }] },
|
||||
ca: { cols: ["#D80621", "#FFFFFF", "#D80621"], sizes: "1fr 2fr 1fr", marks: [{ l: 20, t: 9, w: 7, h: 13, bg: "#D80621", clip: "polygon(50% 0, 100% 100%, 0 100%)" }] },
|
||||
au: { bg: "#00247D", marks: [{ l: 9, t: 0, w: 4, h: 16, bg: "#FFFFFF" }, { l: 0, t: 6, w: 22, h: 4, bg: "#FFFFFF" }, { l: 30, t: 19, w: 7, h: 7, r: "50%", bg: "#FFFFFF" }] },
|
||||
br: { bg: "#009739", marks: [{ l: 9, t: 4, w: 28, h: 24, bg: "#FEDD00", clip: "polygon(50% 0, 100% 50%, 50% 100%, 0 50%)" }, { l: 18, t: 11, w: 10, h: 10, r: "50%", bg: "#012169" }] }
|
||||
};
|
||||
}
|
||||
|
||||
flag(code) {
|
||||
const h = React.createElement;
|
||||
const spec = this.flagSpecs()[code] || {};
|
||||
const style = {
|
||||
width: "46px", height: "32px", borderRadius: "6px", overflow: "hidden",
|
||||
display: "grid", border: "1px solid var(--border)", position: "relative", flex: "none"
|
||||
};
|
||||
let bands = [];
|
||||
if (spec.rows) {
|
||||
bands = spec.rows;
|
||||
style.gridTemplateRows = spec.sizes || "repeat(" + bands.length + ", 1fr)";
|
||||
} else if (spec.cols) {
|
||||
bands = spec.cols;
|
||||
style.gridAutoFlow = "column";
|
||||
style.gridTemplateColumns = spec.sizes || "repeat(" + bands.length + ", 1fr)";
|
||||
} else {
|
||||
style.background = spec.bg || "var(--surface-2)";
|
||||
}
|
||||
const kids = bands.map((c, i) => h("span", { key: "b" + i, style: { background: c, display: "block" } }));
|
||||
(spec.marks || []).forEach((m, i) => kids.push(h("span", {
|
||||
key: "m" + i,
|
||||
style: {
|
||||
position: "absolute", left: m.l + "px", top: m.t + "px", width: m.w + "px", height: m.h + "px",
|
||||
borderRadius: m.r || 0, background: m.bg, clipPath: m.clip || "none", display: "block"
|
||||
}
|
||||
})));
|
||||
return h("span", { style: style }, kids);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const mq = window.matchMedia("(prefers-color-scheme: dark)");
|
||||
this.mq = mq;
|
||||
this.onSys = () => this.setState({ sysDark: mq.matches });
|
||||
mq.addEventListener("change", this.onSys);
|
||||
this.onResize = () => {
|
||||
const n = window.innerWidth < 900;
|
||||
if (n !== this.state.narrow) this.setState({ narrow: n, menu: false });
|
||||
};
|
||||
window.addEventListener("resize", this.onResize);
|
||||
this.onResize();
|
||||
let saved = null;
|
||||
try { saved = localStorage.getItem("veilix.theme"); } catch (e) {}
|
||||
let savedLang = null;
|
||||
try { savedLang = localStorage.getItem("veilix.lang"); } catch (e) {}
|
||||
this.setState({ sysDark: mq.matches, theme: saved || this.props.defaultTheme || "light", lang: savedLang || "ru" }, () => { this.apply(); this.applyLang(); });
|
||||
}
|
||||
|
||||
applyLang() {
|
||||
const lang = this.state.lang || "ru";
|
||||
if (window.VEILIX_APPLY_LANG) window.VEILIX_APPLY_LANG(document.body, lang);
|
||||
else setTimeout(() => this.applyLang(), 150);
|
||||
}
|
||||
|
||||
setLang(l) {
|
||||
try { localStorage.setItem("veilix.lang", l); } catch (e) {}
|
||||
this.setState({ lang: l, langMenu: false }, () => this.applyLang());
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.mq) this.mq.removeEventListener("change", this.onSys);
|
||||
if (this.onResize) window.removeEventListener("resize", this.onResize);
|
||||
}
|
||||
componentDidUpdate() { this.apply(); this.applyLang(); }
|
||||
|
||||
apply() {
|
||||
const theme = this.state.theme || this.props.defaultTheme || "light";
|
||||
const dark = theme === "dark" || (theme === "system" && this.state.sysDark);
|
||||
document.documentElement.setAttribute("data-veilix-theme", dark ? "dark" : "light");
|
||||
}
|
||||
|
||||
setTheme(t) {
|
||||
try { localStorage.setItem("veilix.theme", t); } catch (e) {}
|
||||
this.setState({ theme: t }, () => this.apply());
|
||||
}
|
||||
|
||||
renderVals() {
|
||||
const theme = this.state.theme || this.props.defaultTheme || "light";
|
||||
const sel = "var(--sel)";
|
||||
const names = ["Нидерланды · Амстердам", "Германия · Франкфурт", "США · Нью-Йорк", "Финляндия · Хельсинки", "Турция · Стамбул", "Япония · Токио"];
|
||||
const ready = [true, true, true, false, false, false];
|
||||
const i = this.state.loc;
|
||||
const v = {
|
||||
items: this.data().map((d, n) => ({
|
||||
country: d.country,
|
||||
city: d.city,
|
||||
flag: this.flag(d.code),
|
||||
status: d.ready ? "Доступно в подписке" : "Скоро",
|
||||
badge: d.ready ? "Включено" : "Скоро",
|
||||
statusInk: d.ready ? "var(--accent)" : "var(--ink-3)",
|
||||
statusDot: d.ready ? "var(--accent)" : "transparent",
|
||||
statusRing: d.ready ? "0" : "1.5px solid var(--ink-3)",
|
||||
bg: this.state.loc === n ? sel : "var(--surface)",
|
||||
border: this.state.loc === n ? "var(--accent)" : "var(--border)",
|
||||
dot: this.state.loc === n ? "var(--accent)" : "transparent",
|
||||
dotOff: this.state.loc === n,
|
||||
buy: (e) => { e.stopPropagation(); window.location.href = "MassOrder.dc.html"; },
|
||||
setup: d.setup,
|
||||
month: d.month,
|
||||
buyOff: this.state.loc !== n,
|
||||
pick: () => this.setState({ loc: n })
|
||||
})),
|
||||
pickedTitle: names[i],
|
||||
pickedHint: ready[i] ? "Сервер свободен — доступ выдаём сразу после оплаты" : "Свободного сервера нет: установим лично под вас, доступ в течение 24 часов",
|
||||
openLead: () => this.setState({ lead: true }),
|
||||
navOff: this.state.narrow,
|
||||
burgerOff: !this.state.narrow,
|
||||
menuOff: !(this.state.narrow && this.state.menu),
|
||||
toggleMenu: () => this.setState(st => ({ menu: !st.menu })),
|
||||
closeMenu: () => this.setState({ menu: false }),
|
||||
toggleLangMenu: () => this.setState(st => ({ langMenu: !st.langMenu })),
|
||||
langMenuOff: !this.state.langMenu,
|
||||
langLabel: this.state.lang === "en" ? "EN" : "RU",
|
||||
ruCheck: this.state.lang === "en" ? "" : "✓",
|
||||
enCheck: this.state.lang === "en" ? "✓" : "",
|
||||
setRu: () => this.setLang("ru"),
|
||||
setEn: () => this.setLang("en"),
|
||||
ruBg: this.state.lang === "en" ? "transparent" : sel,
|
||||
enBg: this.state.lang === "en" ? sel : "transparent",
|
||||
ruInk: this.state.lang === "en" ? "var(--ink-3)" : "var(--ink)",
|
||||
enInk: this.state.lang === "en" ? "var(--ink)" : "var(--ink-3)",
|
||||
sendLead: () => this.setState({ leadSent: true }),
|
||||
leadOpen: this.state.lead,
|
||||
leadClosed: !this.state.lead,
|
||||
leadButton: this.state.leadSent ? "Заявка отправлена" : "Отправить",
|
||||
setLight: () => this.setTheme("light"),
|
||||
setDark: () => this.setTheme("dark"),
|
||||
setSystem: () => this.setTheme("system"),
|
||||
lightBg: theme === "light" ? sel : "transparent",
|
||||
darkBg: theme === "dark" ? sel : "transparent",
|
||||
systemBg: theme === "system" ? sel : "transparent",
|
||||
lightInk: theme === "light" ? "var(--ink)" : "var(--ink-3)",
|
||||
darkInk: theme === "dark" ? "var(--ink)" : "var(--ink-3)",
|
||||
systemInk: theme === "system" ? "var(--ink)" : "var(--ink-3)"
|
||||
};
|
||||
for (let n = 0; n < 6; n++) {
|
||||
v["loc" + n + "Bg"] = this.state.loc === n ? sel : "var(--surface)";
|
||||
v["loc" + n + "Border"] = this.state.loc === n ? "var(--accent)" : "var(--border)";
|
||||
v["loc" + n + "Dot"] = this.state.loc === n ? "var(--accent)" : "transparent";
|
||||
v["pickLoc" + n] = () => this.setState({ loc: n });
|
||||
}
|
||||
return v;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,369 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<script src="./support.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<x-dc>
|
||||
<helmet>
|
||||
<link rel="icon" type="image/svg+xml" href="favicon-mass.svg" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin="anonymous" />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Commissioner:wght@300;400;500;600&family=Unbounded:wght@500;600&display=swap" rel="stylesheet" />
|
||||
<script>(function(){if(window.__veilixI18nSrc)return;window.__veilixI18nSrc="./i18n.js?v="+Date.now().toString(36)+Math.random().toString(36).slice(2,8);var s=document.createElement("script");s.src=window.__veilixI18nSrc;document.head.appendChild(s);})();</script>
|
||||
<style>
|
||||
:root{
|
||||
--bg: oklch(99.6% 0.0015 215);
|
||||
--surface: oklch(100% 0 0);
|
||||
--surface-2: oklch(98.4% 0.003 215);
|
||||
--ink: oklch(24% 0.04 204);
|
||||
--ink-2: oklch(45% 0.03 204);
|
||||
--ink-3: oklch(60% 0.025 204);
|
||||
--border: oklch(92% 0.014 200);
|
||||
--accent: oklch(48% 0.105 178);
|
||||
--accent-hover: oklch(41% 0.1 178);
|
||||
--accent-soft: oklch(95.5% 0.032 180);
|
||||
--sel: oklch(96.5% 0.016 190);
|
||||
--on-accent: oklch(99% 0.01 180);
|
||||
--glow-1: oklch(86% 0.085 190);
|
||||
--glow-2: oklch(88% 0.075 205);
|
||||
--glow-3: oklch(89% 0.07 172);
|
||||
--veil: oklch(100% 0 0 / 0.55);
|
||||
--shadow: 0 1px 2px oklch(24% 0.04 204 / 0.05), 0 12px 32px -12px oklch(24% 0.04 204 / 0.14);
|
||||
}
|
||||
:root[data-veilix-theme="dark"]{
|
||||
--bg: oklch(23% 0.026 210);
|
||||
--surface: oklch(26.5% 0.028 210);
|
||||
--surface-2: oklch(30% 0.03 210);
|
||||
--ink: oklch(96% 0.012 196);
|
||||
--ink-2: oklch(80% 0.02 196);
|
||||
--ink-3: oklch(65% 0.025 196);
|
||||
--border: oklch(36% 0.04 204);
|
||||
--accent: oklch(76% 0.105 190);
|
||||
--accent-hover: oklch(83% 0.09 190);
|
||||
--accent-soft: oklch(31% 0.05 190);
|
||||
--sel: oklch(30% 0.03 195);
|
||||
--on-accent: oklch(20% 0.04 204);
|
||||
--glow-1: oklch(52% 0.09 200);
|
||||
--glow-2: oklch(47% 0.085 215);
|
||||
--glow-3: oklch(50% 0.08 180);
|
||||
--veil: oklch(100% 0 0 / 0.07);
|
||||
--shadow: 0 1px 2px oklch(12% 0.03 204 / 0.4), 0 16px 40px -16px oklch(9% 0.03 204 / 0.7);
|
||||
}
|
||||
*{ box-sizing: border-box; }
|
||||
input, textarea, select{ font-size: 16px; }
|
||||
[hidden]{ display: none !important; }
|
||||
body{ margin:0; background: var(--bg); color: var(--ink); font-family:"Commissioner", system-ui, sans-serif; -webkit-font-smoothing: antialiased; }
|
||||
a{ color: var(--accent); text-decoration: none; }
|
||||
a:hover{ color: var(--accent-hover); }
|
||||
</style>
|
||||
</helmet>
|
||||
|
||||
<template id="__bundler_thumbnail" data-bg-color="#e3f4f1">
|
||||
<svg viewBox="0 0 1200 800" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="480" y="290" width="150" height="150" rx="48" fill="#00705e" />
|
||||
<rect x="570" y="360" width="150" height="150" rx="48" fill="#ffffff" fill-opacity="0.62" stroke="#00705e" stroke-width="6" />
|
||||
</svg>
|
||||
</template>
|
||||
<div style="background: var(--bg); color: var(--ink); font-family: 'Commissioner', system-ui, sans-serif; font-weight: 400; letter-spacing: -0.01em; min-height: 100vh;">
|
||||
|
||||
<header style="position: sticky; top: 0; z-index: 40; background: color-mix(in oklab, var(--bg) 82%, transparent); backdrop-filter: blur(20px) saturate(1.4); -webkit-backdrop-filter: blur(20px) saturate(1.4); border-bottom: 1px solid var(--border);">
|
||||
<div style="max-width: 1200px; margin: 0 auto; padding: 14px 24px; min-height: 66px; display: flex; flex-wrap: nowrap; align-items: center; gap: 14px 40px;">
|
||||
<a href="MassMain.dc.html" style="display: flex; align-items: center; gap: 10px; color: var(--ink); flex: none;">
|
||||
<span style="position: relative; width: 28px; height: 28px; display: block; flex: none;">
|
||||
<span style="position: absolute; left: 0; top: 3px; width: 21px; height: 21px; border-radius: 7px; background: linear-gradient(140deg, var(--accent), color-mix(in oklab, var(--accent) 55%, var(--glow-3))); display: block;"></span>
|
||||
<span style="position: absolute; right: 0; bottom: 3px; width: 21px; height: 21px; border-radius: 7px; background: var(--veil); border: 1px solid color-mix(in oklab, var(--accent) 35%, transparent); backdrop-filter: blur(2px); display: block;"></span>
|
||||
</span>
|
||||
<span style="font-family: 'Unbounded', 'Commissioner', sans-serif; font-weight: 600; font-size: 17px; letter-spacing: -0.02em; width: 132px; flex: none; white-space: nowrap;">ВейликсВПН</span>
|
||||
</a>
|
||||
<nav hidden="{{ navOff }}" style="display: flex; flex-wrap: wrap; gap: 10px 28px; font-size: 15px; font-weight: 500; color: var(--ink-2);">
|
||||
<a href="MassPrice.dc.html" style="color: var(--ink-2);" style-hover="color: var(--ink);">Цены</a>
|
||||
<a href="MassLocations.dc.html" style="color: var(--ink-2);" style-hover="color: var(--ink);">Локации</a>
|
||||
<a href="#top" style="color: var(--ink);">Поддержка</a>
|
||||
</nav>
|
||||
<div style="margin-left: auto; display: flex; flex-wrap: wrap; align-items: center; gap: 8px;">
|
||||
<div hidden="{{ navOff }}" style="position: relative;">
|
||||
<button type="button" onClick="{{ toggleLangMenu }}" title="Язык" style="display: flex; align-items: center; gap: 7px; height: 38px; padding: 0 11px; border: 1px solid var(--border); border-radius: 10px; background: transparent; color: var(--ink-2); font-family: inherit; font-size: 14px; cursor: pointer;" style-hover="color: var(--ink); border-color: var(--ink-3);">
|
||||
<span style="position: relative; width: 16px; height: 16px; border-radius: 50%; border: 1.5px solid currentColor; display: block; flex: none;">
|
||||
<span style="position: absolute; left: -1.5px; top: 5.75px; width: 16px; height: 1.5px; background: currentColor; display: block;"></span>
|
||||
<span style="position: absolute; left: 3.25px; top: -1.5px; width: 8px; height: 16px; border-radius: 50%; border: 1.5px solid currentColor; display: block;"></span>
|
||||
</span>
|
||||
<span style="font-variant-numeric: tabular-nums;">{{ langLabel }}</span>
|
||||
</button>
|
||||
<div hidden="{{ langMenuOff }}" style="position: absolute; right: 0; top: 46px; width: 172px; padding: 6px; background: var(--surface); border: 1px solid var(--border); border-radius: 14px; box-shadow: var(--shadow); font-size: 14px;">
|
||||
<button type="button" onClick="{{ setRu }}" style="display: flex; align-items: center; justify-content: space-between; gap: 12px; width: 100%; padding: 9px 10px; border: 0; border-radius: 9px; background: {{ ruBg }}; color: var(--ink); font-family: inherit; font-size: 14px; text-align: left; cursor: pointer;">
|
||||
<span>Русский</span>
|
||||
<span style="color: var(--accent);">{{ ruCheck }}</span>
|
||||
</button>
|
||||
<button type="button" onClick="{{ setEn }}" style="display: flex; align-items: center; justify-content: space-between; gap: 12px; width: 100%; padding: 9px 10px; border: 0; border-radius: 9px; background: {{ enBg }}; color: var(--ink); font-family: inherit; font-size: 14px; text-align: left; cursor: pointer;">
|
||||
<span>English</span>
|
||||
<span style="color: var(--accent);">{{ enCheck }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<a href="MassApp.dc.html" hidden="{{ navOff }}" style="display: flex; align-items: center; justify-content: center; height: 38px; width: 78px; flex: none; box-sizing: border-box; border-radius: 10px; color: var(--ink-2); font-size: 15px;" style-hover="color: var(--ink); background: var(--surface-2);">Войти</a>
|
||||
<a href="MassPrice.dc.html" hidden="{{ navOff }}" style="display: flex; align-items: center; justify-content: center; height: 38px; width: 142px; flex: none; box-sizing: border-box; border-radius: 10px; background: var(--accent); color: var(--on-accent); font-size: 15px; font-weight: 500; box-shadow: 0 6px 18px -8px var(--accent);" style-hover="background: var(--accent-hover); color: var(--on-accent);">Получить VPN</a>
|
||||
<button type="button" hidden="{{ burgerOff }}" onClick="{{ toggleMenu }}" title="Меню" style="display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 5px; width: 42px; height: 38px; border: 1px solid var(--border); border-radius: 10px; background: transparent; color: var(--ink); cursor: pointer;" style-hover="border-color: var(--ink-3);">
|
||||
<span style="width: 18px; height: 2px; border-radius: 2px; background: currentColor; display: block;"></span>
|
||||
<span style="width: 18px; height: 2px; border-radius: 2px; background: currentColor; display: block;"></span>
|
||||
<span style="width: 18px; height: 2px; border-radius: 2px; background: currentColor; display: block;"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div hidden="{{ menuOff }}" style="position: absolute; left: 0; right: 0; top: 100%; z-index: 45; padding: 10px 20px 18px; background: var(--bg); border-bottom: 1px solid var(--border); box-shadow: var(--shadow); display: grid; gap: 2px;">
|
||||
<a href="MassPrice.dc.html" onClick="{{ closeMenu }}" style="padding: 13px 6px; font-size: 17px; color: var(--ink);">Цены</a>
|
||||
<a href="MassLocations.dc.html" onClick="{{ closeMenu }}" style="padding: 13px 6px; font-size: 17px; color: var(--ink);">Локации</a>
|
||||
<a href="MassSupport.dc.html" onClick="{{ closeMenu }}" style="padding: 13px 6px; font-size: 17px; color: var(--ink);">Поддержка</a>
|
||||
<div style="display: flex; align-items: center; justify-content: space-between; gap: 12px; padding-top: 6px; margin-top: 6px; border-top: 1px solid var(--border);">
|
||||
<a href="MassApp.dc.html" onClick="{{ closeMenu }}" style="padding: 13px 6px; font-size: 17px; color: var(--ink);">Войти</a>
|
||||
<div style="display: flex; gap: 6px; flex: none;">
|
||||
<button type="button" onClick="{{ setRu }}" style="padding: 9px 13px; border: 1px solid var(--border); border-radius: 10px; background: {{ ruBg }}; color: var(--ink); font-family: inherit; font-size: 13px; cursor: pointer; white-space: nowrap;">Русский</button>
|
||||
<button type="button" onClick="{{ setEn }}" style="padding: 9px 13px; border: 1px solid var(--border); border-radius: 10px; background: {{ enBg }}; color: var(--ink); font-family: inherit; font-size: 13px; cursor: pointer; white-space: nowrap;">English</button>
|
||||
</div>
|
||||
</div>
|
||||
<a href="MassPrice.dc.html" onClick="{{ closeMenu }}" style="display: flex; align-items: center; justify-content: center; height: 50px; margin-top: 12px; border-radius: 12px; background: var(--accent); color: var(--on-accent); font-size: 16px; font-weight: 500;">Получить VPN</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main id="top" style="max-width: 1000px; margin: 0 auto; padding: 0 24px 80px;">
|
||||
|
||||
<section style="padding: 56px 0 8px;">
|
||||
<h1 style="margin: 0 0 14px; font-size: clamp(34px, 4.4vw, 52px); font-weight: 600; letter-spacing: -0.035em;">Поддержка</h1>
|
||||
<div style="height: 32px;"></div>
|
||||
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 16px;">
|
||||
<a href="#form" style="display: grid; gap: 10px; padding: clamp(20px, 4.8vw, 28px); border-radius: 22px; background: var(--surface); border: 1px solid var(--border); box-shadow: var(--shadow); color: var(--ink);" style-hover="border-color: var(--accent); color: var(--ink);">
|
||||
<span style="font-size: 13px; color: var(--ink-3);">Telegram</span>
|
||||
<span style="font-size: clamp(24px, 3vw, 32px); font-weight: 600; letter-spacing: -0.03em; color: var(--accent);">@veilix_support</span>
|
||||
<span style="font-size: 14px; color: var(--ink-2); font-weight: 300;">Быстрее всего. Можно писать голосом и скидывать скриншоты.</span>
|
||||
</a>
|
||||
<a href="#form" style="display: grid; gap: 10px; padding: clamp(20px, 4.8vw, 28px); border-radius: 22px; background: var(--surface); border: 1px solid var(--border); box-shadow: var(--shadow); color: var(--ink);" style-hover="border-color: var(--accent); color: var(--ink);">
|
||||
<span style="font-size: 13px; color: var(--ink-3);">Почта</span>
|
||||
<span style="font-size: clamp(22px, 2.6vw, 30px); font-weight: 600; letter-spacing: -0.03em; color: var(--accent); word-break: break-word;">help@veilixvpn.cc</span>
|
||||
<span style="font-size: 14px; color: var(--ink-2); font-weight: 300;">Для счетов, возвратов и всего, что нужно письменно.</span>
|
||||
</a>
|
||||
</div>
|
||||
<p style="margin: 16px 0 0; font-size: 14px; color: var(--ink-3);">Отвечаем с 10 до 20 в рабочие дни.</p>
|
||||
</section>
|
||||
|
||||
<section style="padding: 64px 0 0;">
|
||||
<h2 style="margin: 0 0 20px; font-size: clamp(26px, 2.8vw, 34px); font-weight: 600; letter-spacing: -0.03em;">Частые вопросы</h2>
|
||||
|
||||
<div style="display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 22px;">
|
||||
<button type="button" onClick="{{ tab0 }}" style="padding: 9px 16px; border-radius: 10px; border: 1px solid {{ tab0Border }}; background: {{ tab0Bg }}; color: {{ tab0Ink }}; font-family: inherit; font-size: 14px; cursor: pointer;">Подключение</button>
|
||||
<button type="button" onClick="{{ tab1 }}" style="padding: 9px 16px; border-radius: 10px; border: 1px solid {{ tab1Border }}; background: {{ tab1Bg }}; color: {{ tab1Ink }}; font-family: inherit; font-size: 14px; cursor: pointer;">Оплата и продление</button>
|
||||
<button type="button" onClick="{{ tab2 }}" style="padding: 9px 16px; border-radius: 10px; border: 1px solid {{ tab2Border }}; background: {{ tab2Bg }}; color: {{ tab2Ink }}; font-family: inherit; font-size: 14px; cursor: pointer;">Локации и подписка</button>
|
||||
</div>
|
||||
|
||||
<div style="display: grid; gap: 10px;">
|
||||
<sc-for list="{{ items }}" as="item" hint-placeholder-count="4">
|
||||
<div style="border: 1px solid var(--border); border-radius: 16px; background: var(--surface); overflow: hidden;">
|
||||
<button type="button" onClick="{{ item.toggle }}" style="display: flex; align-items: center; gap: 16px; width: 100%; padding: 20px 22px; background: transparent; border: 0; color: var(--ink); font-family: inherit; font-size: 17px; font-weight: 500; text-align: left; cursor: pointer; letter-spacing: -0.015em;">
|
||||
<span style="flex: 1;">{{ item.q }}</span>
|
||||
<span style="flex: none; width: 10px; height: 10px; border-right: 1.5px solid var(--ink-3); border-bottom: 1.5px solid var(--ink-3); transform: {{ item.rot }}; transition: transform 180ms ease; display: block;"></span>
|
||||
</button>
|
||||
<div hidden="{{ item.off }}" style="padding: 0 22px 22px; font-size: 15px; line-height: 1.6; color: var(--ink-2); font-weight: 300; max-width: 62ch;">{{ item.a }}</div>
|
||||
</div>
|
||||
</sc-for>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="form" style="padding: 64px 0 0;">
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 40px; padding: 40px; border-radius: 24px; background: var(--surface); border: 1px solid var(--border); box-shadow: var(--shadow);">
|
||||
<div>
|
||||
<h2 style="margin: 0 0 10px; font-size: clamp(24px, 2.6vw, 30px); font-weight: 600; letter-spacing: -0.03em;">Написать нам</h2>
|
||||
<p style="margin: 0; max-width: 26em; font-size: 15px; line-height: 1.55; color: var(--ink-2); font-weight: 300;">Если подписка уже оформлена, укажите почту, на которую она оформлена — так мы сразу увидим вашу конфигурацию.</p>
|
||||
</div>
|
||||
<div style="display: grid; gap: 10px;">
|
||||
<input type="email" placeholder="Ваша почта" style="height: 48px; padding: 0 14px; border-radius: 12px; border: 1px solid var(--border); background: var(--bg); color: var(--ink); font-family: inherit; font-size: 15px; width: 100%;" />
|
||||
<select style="height: 48px; padding: 0 12px; border-radius: 12px; border: 1px solid var(--border); background: var(--bg); color: var(--ink); font-family: inherit; font-size: 15px; width: 100%;">
|
||||
<option>Тема: подключение</option>
|
||||
<option>Тема: оплата и продление</option>
|
||||
<option>Тема: локации и подписка</option>
|
||||
<option>Тема: другое</option>
|
||||
</select>
|
||||
<textarea rows="5" placeholder="Что произошло" style="padding: 12px 14px; border-radius: 12px; border: 1px solid var(--border); background: var(--bg); color: var(--ink); font-family: inherit; font-size: 15px; resize: vertical; width: 100%;"></textarea>
|
||||
<button type="button" onClick="{{ send }}" style="display: inline-flex; align-items: center; justify-content: center; height: 50px; border-radius: 12px; border: 0; background: var(--accent); color: var(--on-accent); font-family: inherit; font-size: 16px; font-weight: 500; cursor: pointer;" style-hover="background: var(--accent-hover);">{{ sendLabel }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer style="border-top: 1px solid var(--border); background: var(--surface-2);">
|
||||
<div style="max-width: 1200px; margin: 0 auto; padding: 48px clamp(16px, 4vw, 24px) 32px; display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 32px;">
|
||||
<div>
|
||||
<div style="display: flex; align-items: center; gap: 10px; margin-bottom: 14px;">
|
||||
<span style="position: relative; width: 24px; height: 24px; display: block; flex: none;">
|
||||
<span style="position: absolute; left: 0; top: 3px; width: 18px; height: 18px; border-radius: 6px; background: linear-gradient(140deg, var(--accent), color-mix(in oklab, var(--accent) 55%, var(--glow-3))); display: block;"></span>
|
||||
<span style="position: absolute; right: 0; bottom: 3px; width: 18px; height: 18px; border-radius: 6px; background: var(--veil); border: 1px solid color-mix(in oklab, var(--accent) 35%, transparent); display: block;"></span>
|
||||
</span>
|
||||
<span style="font-family: 'Unbounded', 'Commissioner', sans-serif; font-weight: 600; font-size: 15px; letter-spacing: -0.02em;">ВейликсВПН</span>
|
||||
</div>
|
||||
<p style="margin: 0 0 20px; max-width: 26em; font-size: 14px; line-height: 1.55; color: var(--ink-3); font-weight: 300;">VPN, который не нужно отключать. Работаем с 2022 года.</p>
|
||||
<div style="display: flex; flex-wrap: wrap; gap: 6px;">
|
||||
<span style="display: flex; align-items: center; justify-content: center; width: 60px; height: 28px; border-radius: 8px; border: 1px solid var(--border); background: var(--surface); font-size: 11px; letter-spacing: 0.06em; color: var(--ink-3);">VISA</span>
|
||||
<span style="display: flex; align-items: center; justify-content: center; width: 60px; height: 28px; border-radius: 8px; border: 1px solid var(--border); background: var(--surface); font-size: 11px; letter-spacing: 0.06em; color: var(--ink-3);">MC</span>
|
||||
<span style="display: flex; align-items: center; justify-content: center; width: 60px; height: 28px; border-radius: 8px; border: 1px solid var(--border); background: var(--surface); font-size: 11px; letter-spacing: 0.06em; color: var(--ink-3);">МИР</span>
|
||||
<span style="display: flex; align-items: center; justify-content: center; width: 60px; height: 28px; border-radius: 8px; border: 1px solid var(--border); background: var(--surface); font-size: 11px; letter-spacing: 0.06em; color: var(--ink-3);">CRYPTO</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: grid; grid-template-columns: repeat(2, minmax(130px, 1fr)); gap: 24px 32px; align-content: start;">
|
||||
<div style="display: grid; grid-template-columns: minmax(0, 1fr); gap: 10px; align-content: start; font-size: 14px;">
|
||||
<div style="font-size: 13px; color: var(--ink-3); margin-bottom: 2px;">Сервис</div>
|
||||
<a href="MassPrice.dc.html" style="color: var(--ink-2);" style-hover="color: var(--ink);">Цены</a>
|
||||
<a href="MassLocations.dc.html" style="color: var(--ink-2);" style-hover="color: var(--ink);">Локации</a>
|
||||
<a href="#top" style="color: var(--ink-2);" style-hover="color: var(--ink);">Поддержка</a>
|
||||
</div>
|
||||
<div style="display: grid; grid-template-columns: minmax(0, 1fr); gap: 10px; align-content: start; font-size: 14px;">
|
||||
<div style="font-size: 13px; color: var(--ink-3); margin-bottom: 2px;">Документы</div>
|
||||
<a href="#top" style="color: var(--ink-2);" style-hover="color: var(--ink);">Условия использования</a>
|
||||
<a href="#top" style="color: var(--ink-2);" style-hover="color: var(--ink);">Политика конфиденциальности</a>
|
||||
<a href="#top" style="color: var(--ink-2);" style-hover="color: var(--ink);">Возврат средств</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="max-width: 1200px; margin: 0 auto; padding: 0 clamp(16px, 4vw, 24px) 40px; display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 14px;">
|
||||
<div style="font-size: 13px; color: var(--ink-3);">© 2026 ВейликсВПН</div>
|
||||
<div style="display: flex; align-items: center; gap: 8px;">
|
||||
<div style="display: flex; padding: 2px; border-radius: 9px; border: 1px solid var(--border); background: var(--surface); flex: none;">
|
||||
<button type="button" onClick="{{ setLight }}" title="Светлая тема" style="display: flex; align-items: center; justify-content: center; width: 28px; height: 28px; border: 0; border-radius: 7px; background: {{ lightBg }}; color: {{ lightInk }}; cursor: pointer;"><span style="position: relative; width: 14px; height: 14px; display: block;"><span style="position: absolute; left: 3px; top: 3px; width: 8px; height: 8px; border-radius: 50%; background: currentColor; display: block;"></span><span style="position: absolute; left: 0; top: 0; width: 14px; height: 14px; border-radius: 50%; border: 1.5px solid currentColor; opacity: 0.45; display: block;"></span></span></button>
|
||||
<button type="button" onClick="{{ setDark }}" title="Тёмная тема" style="display: flex; align-items: center; justify-content: center; width: 28px; height: 28px; border: 0; border-radius: 7px; background: {{ darkBg }}; color: {{ darkInk }}; cursor: pointer;"><span style="width: 14px; height: 14px; border-radius: 50%; box-shadow: inset -5px -1.5px 0 0 currentColor; display: block;"></span></button>
|
||||
<button type="button" onClick="{{ setSystem }}" title="Как в системе" style="display: flex; align-items: center; justify-content: center; width: 28px; height: 28px; border: 0; border-radius: 7px; background: {{ systemBg }}; color: {{ systemInk }}; cursor: pointer;"><span style="width: 14px; height: 14px; border-radius: 50%; border: 1.5px solid currentColor; background: linear-gradient(90deg, currentColor 50%, transparent 50%); display: block;"></span></button>
|
||||
</div>
|
||||
<div style="display: flex; padding: 2px; border-radius: 9px; border: 1px solid var(--border); background: var(--surface); flex: none;">
|
||||
<button type="button" onClick="{{ setRu }}" style="padding: 0 10px; height: 28px; border: 0; border-radius: 7px; background: {{ ruBg }}; color: {{ ruInk }}; font-family: inherit; font-size: 12px; cursor: pointer;">RU</button>
|
||||
<button type="button" onClick="{{ setEn }}" style="padding: 0 10px; height: 28px; border: 0; border-radius: 7px; background: {{ enBg }}; color: {{ enInk }}; font-family: inherit; font-size: 12px; cursor: pointer;">EN</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</x-dc>
|
||||
<script type="text/x-dc" data-dc-script data-props="{"defaultTheme":{"editor":"enum","options":["light","dark","system"],"default":"light","tsType":"'light' | 'dark' | 'system'","section":"Тема"}}">
|
||||
class Component extends DCLogic {
|
||||
state = { theme: null, sysDark: false, tab: 0, open: 0, sent: false, lang: "ru", narrow: false, menu: false, langMenu: false };
|
||||
|
||||
componentDidMount() {
|
||||
const mq = window.matchMedia("(prefers-color-scheme: dark)");
|
||||
this.mq = mq;
|
||||
this.onSys = () => this.setState({ sysDark: mq.matches });
|
||||
mq.addEventListener("change", this.onSys);
|
||||
this.onResize = () => {
|
||||
const n = window.innerWidth < 900;
|
||||
if (n !== this.state.narrow) this.setState({ narrow: n, menu: false });
|
||||
};
|
||||
window.addEventListener("resize", this.onResize);
|
||||
this.onResize();
|
||||
let saved = null;
|
||||
try { saved = localStorage.getItem("veilix.theme"); } catch (e) {}
|
||||
let savedLang = null;
|
||||
try { savedLang = localStorage.getItem("veilix.lang"); } catch (e) {}
|
||||
this.setState({ sysDark: mq.matches, theme: saved || this.props.defaultTheme || "light", lang: savedLang || "ru" }, () => { this.apply(); this.applyLang(); });
|
||||
}
|
||||
|
||||
applyLang() {
|
||||
const lang = this.state.lang || "ru";
|
||||
if (window.VEILIX_APPLY_LANG) window.VEILIX_APPLY_LANG(document.body, lang);
|
||||
else setTimeout(() => this.applyLang(), 150);
|
||||
}
|
||||
|
||||
setLang(l) {
|
||||
try { localStorage.setItem("veilix.lang", l); } catch (e) {}
|
||||
this.setState({ lang: l, langMenu: false }, () => this.applyLang());
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.mq) this.mq.removeEventListener("change", this.onSys);
|
||||
if (this.onResize) window.removeEventListener("resize", this.onResize);
|
||||
}
|
||||
componentDidUpdate() { this.apply(); this.applyLang(); }
|
||||
|
||||
apply() {
|
||||
const theme = this.state.theme || this.props.defaultTheme || "light";
|
||||
const dark = theme === "dark" || (theme === "system" && this.state.sysDark);
|
||||
document.documentElement.setAttribute("data-veilix-theme", dark ? "dark" : "light");
|
||||
}
|
||||
|
||||
setTheme(t) {
|
||||
try { localStorage.setItem("veilix.theme", t); } catch (e) {}
|
||||
this.setState({ theme: t }, () => this.apply());
|
||||
}
|
||||
|
||||
faq() {
|
||||
return [
|
||||
[
|
||||
["Что делать сразу после оплаты?", "Зайдите в кабинет по почте, выберите страну и скачайте конфигурацию XRAY или отсканируйте QR-код с телефона. Маршрутизация уже настроена, вручную ничего вписывать не нужно."],
|
||||
["Сколько устройств можно подключить?", "До пяти на одну подписку. Страны у устройств могут быть разные: каждое подключается к своей локации."],
|
||||
["Перестал работать один способ подключения.", "Подключение работает на XRAY: откройте кабинет и обновите конфигурацию или смените локацию. Настройки протокола мы обновляем по мере блокировок."],
|
||||
["Нужно ли что-то настраивать вручную?", "Нет. В конфигурации уже прописано, что российские сайты идут напрямую, а зарубежные — через выбранную локацию. Переключать режимы во время работы не нужно."]
|
||||
],
|
||||
[
|
||||
["Сколько стоит подписка?", "699 ₽ в месяц или 6 990 ₽ в год. Годовая выходит по 582 ₽ в месяц и экономит 1 398 ₽. Платы за подключение и доплат за страну нет."],
|
||||
["Как продлевается подписка?", "Списание раз в месяц или раз в год с привязанной карты. Дату окончания и карту видно в кабинете, там же оплату можно поменять или остановить."],
|
||||
["Можно вернуть деньги?", "Если подписка не заработала по нашей вине, возвращаем полностью в течение первых семи дней. Если вы просто передумали — возвращаем остаток неиспользованного периода."],
|
||||
["Чем можно платить?", "Карты российских и зарубежных банков, а также криптовалюта. Все способы доступны на странице оформления заказа."]
|
||||
],
|
||||
[
|
||||
["Сколько стран входит в подписку?", "Тридцать, включая российскую локацию. Все они доступны сразу и не тарифицируются отдельно."],
|
||||
["Можно менять страну сколько угодно?", "Да. Переключение занимает пару секунд и ничего не стоит: список локаций открыт целиком на всё время подписки."],
|
||||
["Зачем в подписке российский адрес?", "Он работает в обе стороны. Из России — чтобы банки, госуслуги и стриминг видели обычное местное подключение. Из другой страны — чтобы те же сервисы открывались так, будто вы дома."],
|
||||
["Чем это отличается от индивидуального IP?", "Здесь адрес общий: на сервере несколько человек, поэтому подписка стоит заметно дешевле. Если нужен адрес, закреплённый только за вами, это отдельные тарифы Вейликс с индивидуальным IP."]
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
renderVals() {
|
||||
const theme = this.state.theme || this.props.defaultTheme || "light";
|
||||
const sel = "var(--sel)";
|
||||
const list = this.faq()[this.state.tab];
|
||||
const v = {
|
||||
items: list.map((row, i) => ({
|
||||
q: row[0],
|
||||
a: row[1],
|
||||
off: this.state.open !== i,
|
||||
rot: this.state.open === i ? "rotate(-135deg) translateY(2px)" : "rotate(45deg) translateY(-2px)",
|
||||
toggle: () => this.setState(s => ({ open: s.open === i ? -1 : i }))
|
||||
})),
|
||||
send: () => this.setState({ sent: true }),
|
||||
navOff: this.state.narrow,
|
||||
burgerOff: !this.state.narrow,
|
||||
menuOff: !(this.state.narrow && this.state.menu),
|
||||
toggleMenu: () => this.setState(st => ({ menu: !st.menu })),
|
||||
closeMenu: () => this.setState({ menu: false }),
|
||||
toggleLangMenu: () => this.setState(st => ({ langMenu: !st.langMenu })),
|
||||
langMenuOff: !this.state.langMenu,
|
||||
langLabel: this.state.lang === "en" ? "EN" : "RU",
|
||||
ruCheck: this.state.lang === "en" ? "" : "✓",
|
||||
enCheck: this.state.lang === "en" ? "✓" : "",
|
||||
setRu: () => this.setLang("ru"),
|
||||
setEn: () => this.setLang("en"),
|
||||
ruBg: this.state.lang === "en" ? "transparent" : sel,
|
||||
enBg: this.state.lang === "en" ? sel : "transparent",
|
||||
ruInk: this.state.lang === "en" ? "var(--ink-3)" : "var(--ink)",
|
||||
enInk: this.state.lang === "en" ? "var(--ink)" : "var(--ink-3)",
|
||||
sendLabel: this.state.sent ? "Отправлено, ответим на почту" : "Отправить",
|
||||
setLight: () => this.setTheme("light"),
|
||||
setDark: () => this.setTheme("dark"),
|
||||
setSystem: () => this.setTheme("system"),
|
||||
lightBg: theme === "light" ? sel : "transparent",
|
||||
darkBg: theme === "dark" ? sel : "transparent",
|
||||
systemBg: theme === "system" ? sel : "transparent",
|
||||
lightInk: theme === "light" ? "var(--ink)" : "var(--ink-3)",
|
||||
darkInk: theme === "dark" ? "var(--ink)" : "var(--ink-3)",
|
||||
systemInk: theme === "system" ? "var(--ink)" : "var(--ink-3)"
|
||||
};
|
||||
for (let i = 0; i < 3; i++) {
|
||||
v["tab" + i] = () => this.setState({ tab: i, open: 0 });
|
||||
v["tab" + i + "Bg"] = this.state.tab === i ? sel : "transparent";
|
||||
v["tab" + i + "Border"] = this.state.tab === i ? "var(--accent)" : "var(--border)";
|
||||
v["tab" + i + "Ink"] = this.state.tab === i ? "var(--ink)" : "var(--ink-2)";
|
||||
}
|
||||
return v;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,281 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<script src="./support.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<x-dc>
|
||||
<helmet>
|
||||
<link rel="icon" type="image/svg+xml" href="favicon.svg" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin="anonymous" />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Commissioner:wght@300;400;500;600&family=Unbounded:wght@500;600&display=swap" rel="stylesheet" />
|
||||
<script>(function(){if(window.__veilixI18nSrc)return;window.__veilixI18nSrc="./i18n.js?v="+Date.now().toString(36)+Math.random().toString(36).slice(2,8);var s=document.createElement("script");s.src=window.__veilixI18nSrc;document.head.appendChild(s);})();</script>
|
||||
<style>
|
||||
:root{
|
||||
--bg: oklch(99% 0.005 296);
|
||||
--surface: oklch(100% 0 0);
|
||||
--surface-2: oklch(97.5% 0.01 296);
|
||||
--ink: oklch(24% 0.04 296);
|
||||
--ink-2: oklch(45% 0.03 296);
|
||||
--ink-3: oklch(60% 0.025 296);
|
||||
--border: oklch(92% 0.014 296);
|
||||
--accent: oklch(45% 0.17 296);
|
||||
--accent-hover: oklch(38% 0.17 296);
|
||||
--accent-soft: oklch(95.5% 0.028 296);
|
||||
--on-accent: oklch(99% 0.01 296);
|
||||
--glow-1: oklch(86% 0.1 296);
|
||||
--glow-2: oklch(89% 0.09 266);
|
||||
--glow-3: oklch(91% 0.08 332);
|
||||
--veil: oklch(100% 0 0 / 0.55);
|
||||
--shadow: 0 1px 2px oklch(24% 0.04 296 / 0.05), 0 12px 32px -12px oklch(24% 0.04 296 / 0.14);
|
||||
}
|
||||
:root[data-veilix-theme="dark"]{
|
||||
--bg: oklch(17% 0.022 296);
|
||||
--surface: oklch(21% 0.026 296);
|
||||
--surface-2: oklch(24% 0.03 296);
|
||||
--ink: oklch(96% 0.01 296);
|
||||
--ink-2: oklch(79% 0.02 296);
|
||||
--ink-3: oklch(63% 0.025 296);
|
||||
--border: oklch(31% 0.032 296);
|
||||
--accent: oklch(74% 0.15 296);
|
||||
--accent-hover: oklch(80% 0.13 296);
|
||||
--accent-soft: oklch(27% 0.05 296);
|
||||
--on-accent: oklch(17% 0.03 296);
|
||||
--glow-1: oklch(45% 0.16 296);
|
||||
--glow-2: oklch(40% 0.14 266);
|
||||
--glow-3: oklch(42% 0.13 332);
|
||||
--veil: oklch(100% 0 0 / 0.07);
|
||||
--shadow: 0 1px 2px oklch(10% 0.02 296 / 0.4), 0 16px 40px -16px oklch(8% 0.02 296 / 0.7);
|
||||
}
|
||||
*{ box-sizing: border-box; }
|
||||
input, textarea, select{ font-size: 16px; }
|
||||
[hidden]{ display: none !important; }
|
||||
body{ margin:0; background: var(--bg); color: var(--ink); font-family:"Commissioner", system-ui, sans-serif; -webkit-font-smoothing: antialiased; }
|
||||
a{ color: var(--accent); text-decoration: none; }
|
||||
a:hover{ color: var(--accent-hover); }
|
||||
input::placeholder, textarea::placeholder{ color: var(--ink-3); }
|
||||
</style>
|
||||
</helmet>
|
||||
|
||||
<template id="__bundler_thumbnail" data-bg-color="#efeaf8">
|
||||
<svg viewBox="0 0 1200 800" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="480" y="290" width="150" height="150" rx="48" fill="#5b32c9" />
|
||||
<rect x="570" y="360" width="150" height="150" rx="48" fill="#ffffff" fill-opacity="0.62" stroke="#5b32c9" stroke-width="6" />
|
||||
</svg>
|
||||
</template>
|
||||
<div style="background: var(--bg); color: var(--ink); font-family: 'Commissioner', system-ui, sans-serif; font-weight: 400; letter-spacing: -0.01em; min-height: 100vh;">
|
||||
|
||||
<header style="border-bottom: 1px solid var(--border); background: var(--bg);">
|
||||
<div style="max-width: 1080px; margin: 0 auto; padding: 16px 24px; display: flex; flex-wrap: wrap; align-items: center; gap: 14px 24px;">
|
||||
<a href="Main.dc.html" style="display: flex; align-items: center; gap: 10px; color: var(--ink); flex: none;">
|
||||
<span style="position: relative; width: 26px; height: 26px; display: block; flex: none;">
|
||||
<span style="position: absolute; left: 0; top: 3px; width: 19px; height: 19px; border-radius: 6px; background: linear-gradient(140deg, var(--accent), color-mix(in oklab, var(--accent) 55%, var(--glow-3))); display: block;"></span>
|
||||
<span style="position: absolute; right: 0; bottom: 3px; width: 19px; height: 19px; border-radius: 6px; background: var(--veil); border: 1px solid color-mix(in oklab, var(--accent) 35%, transparent); display: block;"></span>
|
||||
</span>
|
||||
<span style="font-family: 'Unbounded', 'Commissioner', sans-serif; font-weight: 600; font-size: 16px; letter-spacing: -0.02em;">Вейликс</span>
|
||||
</a>
|
||||
<div style="margin-left: auto; display: flex; align-items: center; gap: 12px; font-size: 14px; color: var(--ink-3);">
|
||||
<button type="button" onClick="{{ toggleLang }}" title="{{ langTitle }}" style="display: flex; align-items: center; gap: 8px; height: 36px; padding: 0 12px; border-radius: 10px; border: 1px solid var(--border); background: transparent; color: var(--ink-2); font-family: inherit; font-size: 14px; cursor: pointer;" style-hover="color: var(--ink); border-color: var(--ink-3);">
|
||||
<span style="position: relative; width: 16px; height: 16px; border-radius: 50%; border: 1.5px solid currentColor; display: block; flex: none;">
|
||||
<span style="position: absolute; left: -1.5px; top: 5.75px; width: 16px; height: 1.5px; background: currentColor; display: block;"></span>
|
||||
<span style="position: absolute; left: 3.25px; top: -1.5px; width: 8px; height: 16px; border-radius: 50%; border: 1.5px solid currentColor; display: block;"></span>
|
||||
</span>
|
||||
<span style="font-variant-numeric: tabular-nums;">{{ langLabel }}</span>
|
||||
</button>
|
||||
<span>Оформление заказа</span>
|
||||
<a href="Price.dc.html" style="color: var(--ink-2);" style-hover="color: var(--ink);">Отменить</a>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main style="max-width: 1080px; margin: 0 auto; padding: 40px 24px 80px; display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 32px; align-items: start;">
|
||||
|
||||
<div style="display: grid; gap: 16px;">
|
||||
|
||||
<div style="border: 1px solid {{ step1Border }}; border-radius: 20px; background: var(--surface); box-shadow: var(--shadow); overflow: hidden;">
|
||||
<div style="display: flex; align-items: center; gap: 14px; padding: 22px 24px;">
|
||||
<span style="display: flex; align-items: center; justify-content: center; width: 26px; height: 26px; border-radius: 50%; background: {{ step1Dot }}; color: {{ step1DotInk }}; font-size: 13px; font-weight: 500; flex: none;">1</span>
|
||||
<span style="flex: 1; font-size: 18px; font-weight: 500; letter-spacing: -0.02em;">Почта</span>
|
||||
<span hidden="{{ emailNotDone }}" style="font-size: 14px; color: var(--ink-2);">{{ emailValue }}</span>
|
||||
<button type="button" hidden="{{ emailNotDone }}" onClick="{{ editEmail }}" style="border: 0; background: transparent; color: var(--accent); font-family: inherit; font-size: 14px; cursor: pointer; padding: 0;">Изменить</button>
|
||||
</div>
|
||||
|
||||
<div hidden="{{ step1Off }}" style="padding: 0 24px 24px;">
|
||||
<p style="margin: 0 0 16px; max-width: 46em; font-size: 15px; line-height: 1.55; color: var(--ink-2); font-weight: 300;">Введите почту — пришлём одноразовый код. Отдельной регистрации нет: если вы у нас уже были, попадёте в свой кабинет, если нет — он создастся сам.</p>
|
||||
<div hidden="{{ codeSent }}" style="display: flex; flex-wrap: wrap; gap: 10px;">
|
||||
<input type="email" value="{{ emailValue }}" onInput="{{ onEmail }}" placeholder="you@example.com" style="flex: 1; min-width: 220px; height: 50px; padding: 0 14px; border-radius: 12px; border: 1px solid var(--border); background: var(--bg); color: var(--ink); font-family: inherit; font-size: 15px;" />
|
||||
<button type="button" onClick="{{ sendCode }}" style="height: 50px; padding: 0 22px; border-radius: 12px; border: 0; background: var(--accent); color: var(--on-accent); font-family: inherit; font-size: 15px; font-weight: 500; cursor: pointer;" style-hover="background: var(--accent-hover);">Прислать код</button>
|
||||
</div>
|
||||
<div hidden="{{ codeNotSent }}" style="display: grid; gap: 12px;">
|
||||
<div style="font-size: 14px; color: var(--ink-2);">{{ codeLine }}</div>
|
||||
<div style="display: flex; flex-wrap: wrap; gap: 10px;">
|
||||
<input type="text" inputmode="numeric" placeholder="6 цифр" style="flex: 1; min-width: 180px; height: 50px; padding: 0 14px; border-radius: 12px; border: 1px solid var(--border); background: var(--bg); color: var(--ink); font-family: inherit; font-size: 17px; letter-spacing: 0.28em; font-variant-numeric: tabular-nums;" />
|
||||
<button type="button" onClick="{{ confirmCode }}" style="height: 50px; padding: 0 22px; border-radius: 12px; border: 0; background: var(--accent); color: var(--on-accent); font-family: inherit; font-size: 15px; font-weight: 500; cursor: pointer;" style-hover="background: var(--accent-hover);">Продолжить</button>
|
||||
</div>
|
||||
<button type="button" onClick="{{ editEmail }}" style="justify-self: start; border: 0; background: transparent; color: var(--ink-3); font-family: inherit; font-size: 14px; cursor: pointer; padding: 0;">Другая почта</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="border: 1px solid {{ step2Border }}; border-radius: 20px; background: var(--surface); box-shadow: var(--shadow); overflow: hidden;">
|
||||
<div style="display: flex; align-items: center; gap: 14px; padding: 22px 24px;">
|
||||
<span style="display: flex; align-items: center; justify-content: center; width: 26px; height: 26px; border-radius: 50%; background: {{ step2Dot }}; color: {{ step2DotInk }}; font-size: 13px; font-weight: 500; flex: none;">2</span>
|
||||
<span style="flex: 1; font-size: 18px; font-weight: 500; letter-spacing: -0.02em;">Оплата</span>
|
||||
</div>
|
||||
|
||||
<div hidden="{{ step2Off }}" style="padding: 0 24px 24px; display: grid; gap: 16px;">
|
||||
<div style="display: flex; flex-wrap: wrap; gap: 8px;">
|
||||
<button type="button" onClick="{{ payCard }}" style="flex: 1; min-width: 140px; padding: 14px 16px; border-radius: 14px; border: 1px solid {{ cardBorder }}; background: {{ cardBg }}; color: var(--ink); font-family: inherit; font-size: 15px; text-align: left; cursor: pointer;">Карта</button>
|
||||
<button type="button" onClick="{{ payMir }}" style="flex: 1; min-width: 140px; padding: 14px 16px; border-radius: 14px; border: 1px solid {{ mirBorder }}; background: {{ mirBg }}; color: var(--ink); font-family: inherit; font-size: 15px; text-align: left; cursor: pointer;">Карта МИР</button>
|
||||
<button type="button" onClick="{{ payCrypto }}" style="flex: 1; min-width: 140px; padding: 14px 16px; border-radius: 14px; border: 1px solid {{ cryptoBorder }}; background: {{ cryptoBg }}; color: var(--ink); font-family: inherit; font-size: 15px; text-align: left; cursor: pointer;">Криптовалюта</button>
|
||||
</div>
|
||||
|
||||
<div hidden="{{ cryptoChosen }}" style="display: grid; gap: 10px;">
|
||||
<input type="text" placeholder="Номер карты" style="height: 50px; padding: 0 14px; border-radius: 12px; border: 1px solid var(--border); background: var(--bg); color: var(--ink); font-family: inherit; font-size: 15px; width: 100%; font-variant-numeric: tabular-nums;" />
|
||||
<div style="display: flex; flex-wrap: wrap; gap: 10px;">
|
||||
<input type="text" placeholder="ММ / ГГ" style="flex: 1; min-width: 120px; height: 50px; padding: 0 14px; border-radius: 12px; border: 1px solid var(--border); background: var(--bg); color: var(--ink); font-family: inherit; font-size: 15px; font-variant-numeric: tabular-nums;" />
|
||||
<input type="text" placeholder="CVC" style="flex: 1; min-width: 120px; height: 50px; padding: 0 14px; border-radius: 12px; border: 1px solid var(--border); background: var(--bg); color: var(--ink); font-family: inherit; font-size: 15px; font-variant-numeric: tabular-nums;" />
|
||||
</div>
|
||||
<div style="font-size: 13px; color: var(--ink-3); line-height: 1.5;">Карта сохранится для продления. Отменить или заменить её можно в кабинете в любой момент.</div>
|
||||
</div>
|
||||
|
||||
<div hidden="{{ cardChosen }}" style="display: grid; gap: 10px; padding: 18px; border-radius: 14px; background: var(--surface-2); border: 1px solid var(--border);">
|
||||
<div style="font-size: 15px;">USDT, BTC или ETH</div>
|
||||
<div style="font-size: 14px; line-height: 1.55; color: var(--ink-2); font-weight: 300;">После нажатия покажем адрес и сумму. При оплате криптой продление не автоматическое — пришлём напоминание за три дня до конца месяца.</div>
|
||||
</div>
|
||||
|
||||
<label style="display: flex; align-items: flex-start; gap: 10px; font-size: 14px; color: var(--ink-2); font-weight: 300; line-height: 1.5; cursor: pointer;">
|
||||
<input type="checkbox" style="margin-top: 3px; width: 16px; height: 16px; accent-color: var(--accent);" />
|
||||
<span>Согласен с условиями использования и политикой конфиденциальности</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<aside style="position: sticky; top: 24px; display: grid; gap: 16px;">
|
||||
<div style="padding: clamp(20px, 4.6vw, 26px); border-radius: 20px; background: var(--surface); border: 1px solid var(--border); box-shadow: var(--shadow);">
|
||||
<div style="font-size: 13px; color: var(--ink-3); margin-bottom: 16px;">Ваш заказ</div>
|
||||
<div style="display: grid; gap: 6px; margin-bottom: 20px;">
|
||||
<div style="font-size: 20px; font-weight: 600; letter-spacing: -0.025em;">Персональный VPN</div>
|
||||
<div style="font-size: 15px; color: var(--ink-2);">Нидерланды · Амстердам</div>
|
||||
<div style="display: inline-flex; align-items: center; gap: 7px; font-size: 13px; color: var(--accent);">
|
||||
<span style="width: 6px; height: 6px; border-radius: 50%; background: var(--accent); display: block;"></span>
|
||||
Сервер свободен, доступ сразу после оплаты
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: grid; gap: 10px; padding-top: 18px; border-top: 1px solid var(--border); font-size: 15px;">
|
||||
<div style="display: flex; justify-content: space-between; gap: 12px; color: var(--ink-2);"><span>Первый месяц</span><span style="font-variant-numeric: tabular-nums;">4 900 ₽</span></div>
|
||||
<div style="display: flex; justify-content: space-between; gap: 12px; color: var(--ink-2);"><span>RU-адрес</span><span>включён</span></div>
|
||||
<div style="display: flex; justify-content: space-between; gap: 12px; color: var(--ink-2);"><span>Устройств</span><span>до 5</span></div>
|
||||
</div>
|
||||
<div style="display: flex; justify-content: space-between; align-items: baseline; gap: 12px; margin-top: 18px; padding-top: 18px; border-top: 1px solid var(--border);">
|
||||
<span style="font-size: 16px; font-weight: 500;">К оплате сегодня</span>
|
||||
<span style="font-size: 26px; font-weight: 600; letter-spacing: -0.03em; font-variant-numeric: tabular-nums;">4 900 ₽</span>
|
||||
</div>
|
||||
<div style="margin-top: 8px; font-size: 13px; color: var(--ink-3);">{{ nextChargeLine }}</div>
|
||||
<button type="button" onClick="{{ pay }}" disabled="{{ payDisabled }}" style="margin-top: 20px; width: 100%; height: 52px; border-radius: 14px; border: 0; background: {{ payBg }}; color: {{ payInk }}; font-family: inherit; font-size: 16px; font-weight: 500; cursor: {{ payCursor }};">{{ payLabel }}</button>
|
||||
<div hidden="{{ paidOff }}" style="margin-top: 14px; padding: 14px; border-radius: 12px; background: var(--accent-soft); font-size: 14px; line-height: 1.5; color: var(--ink);">Оплачено. Ключ уже в кабинете — <a href="App.dc.html">открыть</a>.</div>
|
||||
</div>
|
||||
</aside>
|
||||
</main>
|
||||
</div>
|
||||
</x-dc>
|
||||
<script type="text/x-dc" data-dc-script data-props="{"defaultTheme":{"editor":"enum","options":["light","dark","system"],"default":"light","tsType":"'light' | 'dark' | 'system'","section":"Тема"}}">
|
||||
class Component extends DCLogic {
|
||||
state = { theme: null, sysDark: false, email: "", sent: false, authed: false, method: "card", paid: false, lang: "ru" };
|
||||
|
||||
componentDidMount() {
|
||||
const mq = window.matchMedia("(prefers-color-scheme: dark)");
|
||||
this.mq = mq;
|
||||
this.onSys = () => this.setState({ sysDark: mq.matches });
|
||||
mq.addEventListener("change", this.onSys);
|
||||
let saved = null;
|
||||
try { saved = localStorage.getItem("veilix.theme"); } catch (e) {}
|
||||
let savedLang = null;
|
||||
try { savedLang = localStorage.getItem("veilix.lang"); } catch (e) {}
|
||||
this.setState({ sysDark: mq.matches, theme: saved || this.props.defaultTheme || "light", lang: savedLang || "ru" }, () => { this.apply(); this.applyLang(); });
|
||||
}
|
||||
|
||||
applyLang() {
|
||||
const lang = this.state.lang || "ru";
|
||||
if (window.VEILIX_APPLY_LANG) window.VEILIX_APPLY_LANG(document.body, lang);
|
||||
else setTimeout(() => this.applyLang(), 150);
|
||||
}
|
||||
|
||||
setLang(l) {
|
||||
try { localStorage.setItem("veilix.lang", l); } catch (e) {}
|
||||
this.setState({ lang: l }, () => this.applyLang());
|
||||
}
|
||||
|
||||
componentWillUnmount() { if (this.mq) this.mq.removeEventListener("change", this.onSys); }
|
||||
componentDidUpdate() { this.apply(); this.applyLang(); }
|
||||
|
||||
apply() {
|
||||
const theme = this.state.theme || this.props.defaultTheme || "light";
|
||||
const dark = theme === "dark" || (theme === "system" && this.state.sysDark);
|
||||
document.documentElement.setAttribute("data-veilix-theme", dark ? "dark" : "light");
|
||||
}
|
||||
|
||||
renderVals() {
|
||||
const s = this.state;
|
||||
const authed = s.authed;
|
||||
const nd = new Date();
|
||||
nd.setMonth(nd.getMonth() + 1);
|
||||
const months = ["января", "февраля", "марта", "апреля", "мая", "июня", "июля", "августа", "сентября", "октября", "ноября", "декабря"];
|
||||
const monthsEn = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
|
||||
const en = s.lang === "en";
|
||||
const sel = "var(--accent-soft)";
|
||||
return {
|
||||
emailValue: s.email,
|
||||
toggleLang: () => this.setLang(s.lang === "en" ? "ru" : "en"),
|
||||
langLabel: s.lang === "en" ? "EN" : "RU",
|
||||
langTitle: s.lang === "en" ? "Язык: английский" : "Язык: русский",
|
||||
onEmail: (e) => this.setState({ email: e.target.value }),
|
||||
sendCode: () => this.setState({ sent: true }),
|
||||
confirmCode: () => this.setState({ authed: true }),
|
||||
editEmail: () => this.setState({ sent: false, authed: false }),
|
||||
codeSent: s.sent,
|
||||
codeNotSent: !s.sent,
|
||||
emailNotDone: !authed,
|
||||
step1Off: authed,
|
||||
step1Border: authed ? "var(--border)" : "var(--accent)",
|
||||
step1Dot: authed ? "var(--accent)" : "var(--accent-soft)",
|
||||
step1DotInk: authed ? "var(--on-accent)" : "var(--accent)",
|
||||
step2Off: !authed,
|
||||
step2Border: authed ? "var(--accent)" : "var(--border)",
|
||||
step2Dot: authed ? "var(--accent-soft)" : "var(--surface-2)",
|
||||
step2DotInk: authed ? "var(--accent)" : "var(--ink-3)",
|
||||
payCard: () => this.setState({ method: "card" }),
|
||||
payMir: () => this.setState({ method: "mir" }),
|
||||
payCrypto: () => this.setState({ method: "crypto" }),
|
||||
cardBg: s.method === "card" ? sel : "transparent",
|
||||
mirBg: s.method === "mir" ? sel : "transparent",
|
||||
cryptoBg: s.method === "crypto" ? sel : "transparent",
|
||||
cardBorder: s.method === "card" ? "var(--accent)" : "var(--border)",
|
||||
mirBorder: s.method === "mir" ? "var(--accent)" : "var(--border)",
|
||||
cryptoBorder: s.method === "crypto" ? "var(--accent)" : "var(--border)",
|
||||
cardChosen: s.method !== "crypto",
|
||||
cryptoChosen: s.method === "crypto",
|
||||
nextCharge: "первое списание " + nd.getDate() + " " + months[nd.getMonth()],
|
||||
nextChargeLine: en
|
||||
? "Then $24 per month, first charge on " + nd.getDate() + " " + monthsEn[nd.getMonth()]
|
||||
: "Далее 2 499 ₽ в месяц, первое списание " + nd.getDate() + " " + months[nd.getMonth()],
|
||||
codeLine: en
|
||||
? "The code has been sent to " + s.email + ". It is valid for 10 minutes."
|
||||
: "Код отправлен на " + s.email + ". Он действует 10 минут.",
|
||||
pay: () => { if (authed) this.setState({ paid: true }); },
|
||||
payDisabled: !authed,
|
||||
payBg: authed ? "var(--accent)" : "var(--surface-2)",
|
||||
payInk: authed ? "var(--on-accent)" : "var(--ink-3)",
|
||||
payCursor: authed ? "pointer" : "not-allowed",
|
||||
payLabel: s.paid ? "Оплачено" : (s.method === "crypto" ? "Показать адрес для оплаты" : "Оплатить 4 900 ₽"),
|
||||
paidOff: !s.paid
|
||||
};
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,501 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<script src="./support.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<x-dc>
|
||||
<helmet>
|
||||
<link rel="icon" type="image/svg+xml" href="favicon.svg" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin="anonymous" />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Commissioner:wght@300;400;500;600&family=Unbounded:wght@500;600&display=swap" rel="stylesheet" />
|
||||
<script>(function(){if(window.__veilixI18nSrc)return;window.__veilixI18nSrc="./i18n.js?v="+Date.now().toString(36)+Math.random().toString(36).slice(2,8);var s=document.createElement("script");s.src=window.__veilixI18nSrc;document.head.appendChild(s);})();</script>
|
||||
<style>
|
||||
:root{
|
||||
--bg: oklch(99% 0.005 296);
|
||||
--surface: oklch(100% 0 0);
|
||||
--surface-2: oklch(97.5% 0.01 296);
|
||||
--ink: oklch(24% 0.04 296);
|
||||
--ink-2: oklch(45% 0.03 296);
|
||||
--ink-3: oklch(60% 0.025 296);
|
||||
--border: oklch(92% 0.014 296);
|
||||
--accent: oklch(45% 0.17 296);
|
||||
--accent-hover: oklch(38% 0.17 296);
|
||||
--accent-soft: oklch(95.5% 0.028 296);
|
||||
--on-accent: oklch(99% 0.01 296);
|
||||
--glow-1: oklch(86% 0.1 296);
|
||||
--glow-2: oklch(89% 0.09 266);
|
||||
--glow-3: oklch(91% 0.08 332);
|
||||
--veil: oklch(100% 0 0 / 0.55);
|
||||
--shadow: 0 1px 2px oklch(24% 0.04 296 / 0.05), 0 12px 32px -12px oklch(24% 0.04 296 / 0.14);
|
||||
}
|
||||
:root[data-veilix-theme="dark"]{
|
||||
--bg: oklch(17% 0.022 296);
|
||||
--surface: oklch(21% 0.026 296);
|
||||
--surface-2: oklch(24% 0.03 296);
|
||||
--ink: oklch(96% 0.01 296);
|
||||
--ink-2: oklch(79% 0.02 296);
|
||||
--ink-3: oklch(63% 0.025 296);
|
||||
--border: oklch(31% 0.032 296);
|
||||
--accent: oklch(74% 0.15 296);
|
||||
--accent-hover: oklch(80% 0.13 296);
|
||||
--accent-soft: oklch(27% 0.05 296);
|
||||
--on-accent: oklch(17% 0.03 296);
|
||||
--glow-1: oklch(45% 0.16 296);
|
||||
--glow-2: oklch(40% 0.14 266);
|
||||
--glow-3: oklch(42% 0.13 332);
|
||||
--veil: oklch(100% 0 0 / 0.07);
|
||||
--shadow: 0 1px 2px oklch(10% 0.02 296 / 0.4), 0 16px 40px -16px oklch(8% 0.02 296 / 0.7);
|
||||
}
|
||||
*{ box-sizing: border-box; }
|
||||
input, textarea, select{ font-size: 16px; }
|
||||
[hidden]{ display: none !important; }
|
||||
body{ margin:0; background: var(--bg); color: var(--ink); font-family:"Commissioner", system-ui, sans-serif; -webkit-font-smoothing: antialiased; }
|
||||
a{ color: var(--accent); text-decoration: none; }
|
||||
a:hover{ color: var(--accent-hover); }
|
||||
@keyframes veilDrift{
|
||||
0%{ transform: translate3d(0,0,0) scale(1); }
|
||||
50%{ transform: translate3d(2%,-3%,0) scale(1.06); }
|
||||
100%{ transform: translate3d(0,0,0) scale(1); }
|
||||
}
|
||||
</style>
|
||||
</helmet>
|
||||
|
||||
<template id="__bundler_thumbnail" data-bg-color="#efeaf8">
|
||||
<svg viewBox="0 0 1200 800" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="480" y="290" width="150" height="150" rx="48" fill="#5b32c9" />
|
||||
<rect x="570" y="360" width="150" height="150" rx="48" fill="#ffffff" fill-opacity="0.62" stroke="#5b32c9" stroke-width="6" />
|
||||
</svg>
|
||||
</template>
|
||||
<div style="background: var(--bg); color: var(--ink); font-family: 'Commissioner', system-ui, sans-serif; font-weight: 400; letter-spacing: -0.01em; min-height: 100vh;">
|
||||
|
||||
<header style="position: sticky; top: 0; z-index: 40; background: color-mix(in oklab, var(--bg) 82%, transparent); backdrop-filter: blur(20px) saturate(1.4); -webkit-backdrop-filter: blur(20px) saturate(1.4); border-bottom: 1px solid var(--border);">
|
||||
<div style="max-width: 1200px; margin: 0 auto; padding: 14px 24px; min-height: 66px; display: flex; flex-wrap: nowrap; align-items: center; gap: 14px 40px;">
|
||||
<a href="Main.dc.html" style="display: flex; align-items: center; gap: 10px; color: var(--ink); flex: none;">
|
||||
<span style="position: relative; width: 28px; height: 28px; display: block; flex: none;">
|
||||
<span style="position: absolute; left: 0; top: 3px; width: 21px; height: 21px; border-radius: 7px; background: linear-gradient(140deg, var(--accent), color-mix(in oklab, var(--accent) 55%, var(--glow-3))); display: block;"></span>
|
||||
<span style="position: absolute; right: 0; bottom: 3px; width: 21px; height: 21px; border-radius: 7px; background: var(--veil); border: 1px solid color-mix(in oklab, var(--accent) 35%, transparent); backdrop-filter: blur(2px); display: block;"></span>
|
||||
</span>
|
||||
<span style="font-family: 'Unbounded', 'Commissioner', sans-serif; font-weight: 600; font-size: 17px; letter-spacing: -0.02em; width: 88px; flex: none; white-space: nowrap;">Вейликс</span>
|
||||
</a>
|
||||
<nav hidden="{{ navOff }}" style="display: flex; flex-wrap: wrap; gap: 10px 28px; font-size: 15px; font-weight: 500; color: var(--ink-2);">
|
||||
<a href="#top" style="color: var(--ink);">Цены</a>
|
||||
<a href="Locations.dc.html" style="color: var(--ink-2);" style-hover="color: var(--ink);">Локации</a>
|
||||
<a href="Support.dc.html" style="color: var(--ink-2);" style-hover="color: var(--ink);">Поддержка</a>
|
||||
</nav>
|
||||
<div style="margin-left: auto; display: flex; flex-wrap: wrap; align-items: center; gap: 8px;">
|
||||
<div hidden="{{ navOff }}" style="position: relative;">
|
||||
<button type="button" onClick="{{ toggleLangMenu }}" title="Язык" style="display: flex; align-items: center; gap: 7px; height: 38px; padding: 0 11px; border: 1px solid var(--border); border-radius: 10px; background: transparent; color: var(--ink-2); font-family: inherit; font-size: 14px; cursor: pointer;" style-hover="color: var(--ink); border-color: var(--ink-3);">
|
||||
<span style="position: relative; width: 16px; height: 16px; border-radius: 50%; border: 1.5px solid currentColor; display: block; flex: none;">
|
||||
<span style="position: absolute; left: -1.5px; top: 5.75px; width: 16px; height: 1.5px; background: currentColor; display: block;"></span>
|
||||
<span style="position: absolute; left: 3.25px; top: -1.5px; width: 8px; height: 16px; border-radius: 50%; border: 1.5px solid currentColor; display: block;"></span>
|
||||
</span>
|
||||
<span style="font-variant-numeric: tabular-nums;">{{ langLabel }}</span>
|
||||
</button>
|
||||
<div hidden="{{ langMenuOff }}" style="position: absolute; right: 0; top: 46px; width: 172px; padding: 6px; background: var(--surface); border: 1px solid var(--border); border-radius: 14px; box-shadow: var(--shadow); font-size: 14px;">
|
||||
<button type="button" onClick="{{ setRu }}" style="display: flex; align-items: center; justify-content: space-between; gap: 12px; width: 100%; padding: 9px 10px; border: 0; border-radius: 9px; background: {{ ruBg }}; color: var(--ink); font-family: inherit; font-size: 14px; text-align: left; cursor: pointer;">
|
||||
<span>Русский</span>
|
||||
<span style="color: var(--accent);">{{ ruCheck }}</span>
|
||||
</button>
|
||||
<button type="button" onClick="{{ setEn }}" style="display: flex; align-items: center; justify-content: space-between; gap: 12px; width: 100%; padding: 9px 10px; border: 0; border-radius: 9px; background: {{ enBg }}; color: var(--ink); font-family: inherit; font-size: 14px; text-align: left; cursor: pointer;">
|
||||
<span>English</span>
|
||||
<span style="color: var(--accent);">{{ enCheck }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<a href="App.dc.html" hidden="{{ navOff }}" style="display: flex; align-items: center; justify-content: center; height: 38px; width: 78px; flex: none; box-sizing: border-box; border-radius: 10px; color: var(--ink-2); font-size: 15px;" style-hover="color: var(--ink); background: var(--surface-2);">Войти</a>
|
||||
<a href="#locations" hidden="{{ navOff }}" style="display: flex; align-items: center; justify-content: center; height: 38px; width: 142px; flex: none; box-sizing: border-box; border-radius: 10px; background: var(--accent); color: var(--on-accent); font-size: 15px; font-weight: 500; box-shadow: 0 6px 18px -8px var(--accent);" style-hover="background: var(--accent-hover); color: var(--on-accent);">Получить VPN</a>
|
||||
<button type="button" hidden="{{ burgerOff }}" onClick="{{ toggleMenu }}" title="Меню" style="display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 5px; width: 42px; height: 38px; border: 1px solid var(--border); border-radius: 10px; background: transparent; color: var(--ink); cursor: pointer;" style-hover="border-color: var(--ink-3);">
|
||||
<span style="width: 18px; height: 2px; border-radius: 2px; background: currentColor; display: block;"></span>
|
||||
<span style="width: 18px; height: 2px; border-radius: 2px; background: currentColor; display: block;"></span>
|
||||
<span style="width: 18px; height: 2px; border-radius: 2px; background: currentColor; display: block;"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div hidden="{{ menuOff }}" style="position: absolute; left: 0; right: 0; top: 100%; z-index: 45; padding: 10px 20px 18px; background: var(--bg); border-bottom: 1px solid var(--border); box-shadow: var(--shadow); display: grid; gap: 2px;">
|
||||
<a href="Price.dc.html" onClick="{{ closeMenu }}" style="padding: 13px 6px; font-size: 17px; color: var(--ink);">Цены</a>
|
||||
<a href="Locations.dc.html" onClick="{{ closeMenu }}" style="padding: 13px 6px; font-size: 17px; color: var(--ink);">Локации</a>
|
||||
<a href="Support.dc.html" onClick="{{ closeMenu }}" style="padding: 13px 6px; font-size: 17px; color: var(--ink);">Поддержка</a>
|
||||
<div style="display: flex; align-items: center; justify-content: space-between; gap: 12px; padding-top: 6px; margin-top: 6px; border-top: 1px solid var(--border);">
|
||||
<a href="App.dc.html" onClick="{{ closeMenu }}" style="padding: 13px 6px; font-size: 17px; color: var(--ink);">Войти</a>
|
||||
<div style="display: flex; gap: 6px; flex: none;">
|
||||
<button type="button" onClick="{{ setRu }}" style="padding: 9px 13px; border: 1px solid var(--border); border-radius: 10px; background: {{ ruBg }}; color: var(--ink); font-family: inherit; font-size: 13px; cursor: pointer; white-space: nowrap;">Русский</button>
|
||||
<button type="button" onClick="{{ setEn }}" style="padding: 9px 13px; border: 1px solid var(--border); border-radius: 10px; background: {{ enBg }}; color: var(--ink); font-family: inherit; font-size: 13px; cursor: pointer; white-space: nowrap;">English</button>
|
||||
</div>
|
||||
</div>
|
||||
<a href="#locations" onClick="{{ closeMenu }}" style="display: flex; align-items: center; justify-content: center; height: 50px; margin-top: 12px; border-radius: 12px; background: var(--accent); color: var(--on-accent); font-size: 16px; font-weight: 500;">Получить VPN</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main id="top" style="max-width: 1200px; margin: 0 auto; padding: 0 24px 80px;">
|
||||
|
||||
<section style="position: relative; overflow: hidden; isolation: isolate; -webkit-transform: translateZ(0); transform: translateZ(0); margin-top: 40px; border-radius: 28px; border: 1px solid var(--border); background: var(--surface); box-shadow: var(--shadow);">
|
||||
<div style="position: absolute; right: min(-80px, calc(-140px + 11vw)); top: min(-120px, calc(-180px + 11vw)); width: clamp(180px, 38vw, 420px); height: clamp(180px, 38vw, 420px); border-radius: 50%; background: radial-gradient(circle at 50% 50%, var(--glow-1) 0%, color-mix(in oklab, var(--glow-1) 35%, transparent) 55%, transparent 78%); filter: blur(70px); opacity: 0.5; animation: veilDrift 18s ease-in-out infinite;"></div>
|
||||
<div style="position: absolute; left: min(-60px, calc(-120px + 11vw)); bottom: min(-140px, calc(-200px + 11vw)); width: clamp(150px, 32vw, 360px); height: clamp(150px, 32vw, 360px); border-radius: 50%; background: radial-gradient(circle at 50% 50%, var(--glow-3) 0%, color-mix(in oklab, var(--glow-3) 35%, transparent) 55%, transparent 78%); filter: blur(70px); opacity: 0.4;"></div>
|
||||
<div style="position: relative; display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: clamp(28px, 5vw, 48px); padding: clamp(28px, 6vw, 56px) clamp(20px, 5vw, 48px);">
|
||||
<div>
|
||||
<div style="display: flex; flex-wrap: wrap; align-items: baseline; gap: 12px;">
|
||||
<span style="font-size: clamp(56px, 8vw, 96px); font-weight: 600; letter-spacing: -0.045em; line-height: 0.95; font-variant-numeric: tabular-nums;">4 900 ₽</span>
|
||||
<span style="font-size: 18px; color: var(--ink-2);">первый месяц</span>
|
||||
</div>
|
||||
<div style="margin-top: 16px; font-size: 20px; color: var(--ink-2); font-variant-numeric: tabular-nums;">далее 2 499 ₽ в месяц</div>
|
||||
<p style="margin: 20px 0 0; max-width: 30em; font-size: 15px; line-height: 1.55; color: var(--ink-3); font-weight: 300;">В первый месяц входит подготовка сервера под вас. Дальше — только абонентская плата, отменить можно в кабинете.</p>
|
||||
</div>
|
||||
<div style="display: grid; gap: 14px; align-content: start;">
|
||||
<div style="font-size: 15px; font-weight: 500;">Что входит</div>
|
||||
<div style="display: grid; gap: 12px; font-size: 15px; color: var(--ink-2); font-weight: 300;">
|
||||
<div style="display: flex; gap: 12px;"><span style="color: var(--accent);">—</span><span>Индивидуальный IP в выбранной стране, только ваш</span></div>
|
||||
<div style="display: flex; gap: 12px;"><span style="color: var(--accent);">—</span><span>Выделенный RU-адрес: прямое соединение и мост в обе стороны</span></div>
|
||||
<div style="display: flex; gap: 12px;"><span style="color: var(--accent);">—</span><span>До 5 устройств с одним и тем же адресом</span></div>
|
||||
<div style="display: flex; gap: 12px;"><span style="color: var(--accent);">—</span><span>Два типа подключения: WireGuard и Xray</span></div>
|
||||
<div style="display: flex; gap: 12px;"><span style="color: var(--accent);">—</span><span>Умная маршрутизация RU / Глобал без переключений</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="locations" style="padding: 72px 0 0;">
|
||||
<div style="display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; gap: 16px; margin-bottom: 8px;">
|
||||
<h2 style="margin: 0; font-size: clamp(28px, 3vw, 38px); font-weight: 600; letter-spacing: -0.03em;">Выберите локацию</h2>
|
||||
</div>
|
||||
<p style="margin: 0 0 28px; max-width: 52em; font-size: 16px; line-height: 1.55; color: var(--ink-2); font-weight: 300;">Где-то сервер уже стоит и доступ выдаётся сразу. Если свободного нет, мы устанавливаем сервер лично под вас — на это уходит до 24 часов, и вы видите этот статус честно, до оплаты.</p>
|
||||
<p style="margin: -16px 0 28px; max-width: 52em; font-size: 15px; line-height: 1.55; color: var(--ink-2); font-weight: 300;">Базовая цена — 4 900 ₽ первый месяц и 2 499 ₽ далее. В редких локациях она выше и указана на карточке.</p>
|
||||
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 12px;">
|
||||
<sc-for list="{{ items }}" as="loc" hint-placeholder-count="12">
|
||||
<div role="button" tabindex="0" onClick="{{ loc.pick }}" style="display: grid; align-content: start; gap: 14px; padding: 22px; border-radius: 22px; border: 1px solid {{ loc.border }}; background: {{ loc.bg }}; color: var(--ink); cursor: pointer;" style-hover="border-color: var(--ink-3);">
|
||||
<span style="display: flex; align-items: center; justify-content: space-between; gap: 14px;">
|
||||
{{ loc.flag }}
|
||||
<span style="display: flex; align-items: center; gap: 12px; flex: none; min-height: 32px;">
|
||||
<span onClick="{{ loc.buy }}" hidden="{{ loc.buyOff }}" style="display: flex; align-items: center; justify-content: center; height: 32px; padding: 0 16px; border-radius: 10px; background: var(--accent); color: var(--on-accent); font-size: 14px; font-weight: 500; cursor: pointer;">Купить</span>
|
||||
<span hidden="{{ loc.dotOff }}" style="width: 11px; height: 11px; border-radius: 50%; background: {{ loc.dot }}; display: block; flex: none;"></span>
|
||||
</span>
|
||||
</span>
|
||||
<span style="display: grid; gap: 5px;">
|
||||
<span style="font-size: 19px; font-weight: 500; letter-spacing: -0.02em;">{{ loc.country }}</span>
|
||||
<span style="font-size: 14px; color: var(--ink-3);">{{ loc.city }}</span>
|
||||
</span>
|
||||
<span style="display: inline-flex; align-items: center; gap: 8px; font-size: 13px; color: {{ loc.statusInk }};">
|
||||
<span style="width: 6px; height: 6px; border-radius: 50%; background: {{ loc.statusDot }}; border: {{ loc.statusRing }}; display: block;"></span>
|
||||
{{ loc.status }}
|
||||
</span>
|
||||
<span style="display: grid; gap: 12px; padding-top: 14px; border-top: 1px solid var(--border);">
|
||||
<span style="display: flex; flex-wrap: wrap; align-items: baseline; gap: 6px; font-size: 13px; color: var(--ink-2); font-variant-numeric: tabular-nums;">
|
||||
<span style="font-size: 17px; font-weight: 500; color: var(--ink);">{{ loc.setup }}</span>
|
||||
первый месяц · далее
|
||||
<span style="font-weight: 500; color: var(--ink);">{{ loc.month }}</span>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</sc-for>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
|
||||
<section style="padding: 72px 0 0;">
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 16px;">
|
||||
<div style="padding: clamp(16px, 4vw, 32px); border-radius: 22px; background: var(--surface); border: 1px solid var(--border); box-shadow: var(--shadow);">
|
||||
<h3 style="margin: 0 0 10px; font-size: 22px; font-weight: 600; letter-spacing: -0.025em;">Нужно несколько локаций</h3>
|
||||
<p style="margin: 0; font-size: 15px; line-height: 1.55; color: var(--ink-2); font-weight: 300;">Каждый тариф — отдельный индивидуальный IP. Берёте второй тариф в другой стране, оба ключа лежат в одном кабинете и продлеваются независимо.</p>
|
||||
</div>
|
||||
<div style="position: relative; isolation: isolate; padding: clamp(16px, 4vw, 32px); border-radius: 22px; background: var(--surface); border: 1px solid var(--border); box-shadow: var(--shadow);">
|
||||
<div style="position: absolute; inset: 0; border-radius: 22px; overflow: hidden; pointer-events: none; -webkit-mask-image: -webkit-radial-gradient(white, black); mask-image: radial-gradient(white, black);">
|
||||
<div style="position: absolute; right: -70px; bottom: -90px; width: 260px; height: 260px; border-radius: 50%; background: var(--glow-1); filter: blur(60px); opacity: 0.5;"></div>
|
||||
</div>
|
||||
<div style="position: relative;">
|
||||
<h3 id="lead" style="margin: 0 0 10px; font-size: 22px; font-weight: 600; letter-spacing: -0.025em; scroll-margin-top: 90px;">Нужны индивидуальные условия</h3>
|
||||
<p style="margin: 0 0 20px; font-size: 15px; line-height: 1.55; color: var(--ink-2); font-weight: 300;">Команда, много адресов, редкая страна или свой сценарий маршрутизации. Оставьте заявку — ответим в течение дня.</p>
|
||||
<button type="button" onClick="{{ openLead }}" hidden="{{ leadOpen }}" style="display: inline-flex; align-items: center; height: 48px; padding: 0 22px; border-radius: 12px; border: 1px solid var(--accent); background: transparent; color: var(--accent); font-family: inherit; font-size: 15px; font-weight: 500; cursor: pointer;" style-hover="background: var(--accent-soft);">Оставить заявку</button>
|
||||
<div hidden="{{ leadClosed }}" style="display: grid; gap: 10px; max-width: 420px;">
|
||||
<input type="text" placeholder="Имя" style="height: 48px; padding: 0 14px; border-radius: 12px; border: 1px solid var(--border); background: var(--bg); color: var(--ink); font-family: inherit; font-size: 15px; width: 100%;" />
|
||||
<input type="email" placeholder="Email" style="height: 48px; padding: 0 14px; border-radius: 12px; border: 1px solid var(--border); background: var(--bg); color: var(--ink); font-family: inherit; font-size: 15px; width: 100%;" />
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 10px;">
|
||||
<input type="text" placeholder="Telegram" style="height: 48px; padding: 0 14px; border-radius: 12px; border: 1px solid var(--border); background: var(--bg); color: var(--ink); font-family: inherit; font-size: 15px; width: 100%;" />
|
||||
<input type="text" placeholder="WhatsApp" style="height: 48px; padding: 0 14px; border-radius: 12px; border: 1px solid var(--border); background: var(--bg); color: var(--ink); font-family: inherit; font-size: 15px; width: 100%;" />
|
||||
</div>
|
||||
<textarea rows="3" placeholder="Коротко о задаче" style="padding: 12px 14px; border-radius: 12px; border: 1px solid var(--border); background: var(--bg); color: var(--ink); font-family: inherit; font-size: 15px; resize: vertical; width: 100%;"></textarea>
|
||||
<button type="button" onClick="{{ sendLead }}" style="display: inline-flex; align-items: center; justify-content: center; height: 48px; padding: 0 22px; border-radius: 12px; border: 0; background: var(--accent); color: var(--on-accent); font-family: inherit; font-size: 15px; font-weight: 500; cursor: pointer;" style-hover="background: var(--accent-hover);">{{ leadButton }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer style="border-top: 1px solid var(--border); background: var(--surface-2);">
|
||||
<div style="max-width: 1200px; margin: 0 auto; padding: 48px clamp(16px, 4vw, 24px) 32px; display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 32px;">
|
||||
<div>
|
||||
<div style="display: flex; align-items: center; gap: 10px; margin-bottom: 14px;">
|
||||
<span style="position: relative; width: 24px; height: 24px; display: block; flex: none;">
|
||||
<span style="position: absolute; left: 0; top: 3px; width: 18px; height: 18px; border-radius: 6px; background: linear-gradient(140deg, var(--accent), color-mix(in oklab, var(--accent) 55%, var(--glow-3))); display: block;"></span>
|
||||
<span style="position: absolute; right: 0; bottom: 3px; width: 18px; height: 18px; border-radius: 6px; background: var(--veil); border: 1px solid color-mix(in oklab, var(--accent) 35%, transparent); display: block;"></span>
|
||||
</span>
|
||||
<span style="font-family: 'Unbounded', 'Commissioner', sans-serif; font-weight: 600; font-size: 15px; letter-spacing: -0.02em;">Вейликс</span>
|
||||
</div>
|
||||
<p style="margin: 0 0 20px; max-width: 26em; font-size: 14px; line-height: 1.55; color: var(--ink-3); font-weight: 300;">Персональные VPN-серверы с индивидуальным IP. Работаем с 2022 года.</p>
|
||||
<div style="display: flex; flex-wrap: wrap; gap: 6px;">
|
||||
<span style="display: flex; align-items: center; justify-content: center; width: 60px; height: 28px; border-radius: 8px; border: 1px solid var(--border); background: var(--surface); font-size: 11px; letter-spacing: 0.06em; color: var(--ink-3);">VISA</span>
|
||||
<span style="display: flex; align-items: center; justify-content: center; width: 60px; height: 28px; border-radius: 8px; border: 1px solid var(--border); background: var(--surface); font-size: 11px; letter-spacing: 0.06em; color: var(--ink-3);">MC</span>
|
||||
<span style="display: flex; align-items: center; justify-content: center; width: 60px; height: 28px; border-radius: 8px; border: 1px solid var(--border); background: var(--surface); font-size: 11px; letter-spacing: 0.06em; color: var(--ink-3);">МИР</span>
|
||||
<span style="display: flex; align-items: center; justify-content: center; width: 60px; height: 28px; border-radius: 8px; border: 1px solid var(--border); background: var(--surface); font-size: 11px; letter-spacing: 0.06em; color: var(--ink-3);">CRYPTO</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: grid; grid-template-columns: repeat(2, minmax(130px, 1fr)); gap: 24px 32px; align-content: start;">
|
||||
<div style="display: grid; grid-template-columns: minmax(0, 1fr); gap: 10px; align-content: start; font-size: 14px;">
|
||||
<div style="font-size: 13px; color: var(--ink-3); margin-bottom: 2px;">Сервис</div>
|
||||
<a href="#top" style="color: var(--ink-2);" style-hover="color: var(--ink);">Цены</a>
|
||||
<a href="Locations.dc.html" style="color: var(--ink-2);" style-hover="color: var(--ink);">Локации</a>
|
||||
<a href="Support.dc.html" style="color: var(--ink-2);" style-hover="color: var(--ink);">Поддержка</a>
|
||||
</div>
|
||||
<div style="display: grid; grid-template-columns: minmax(0, 1fr); gap: 10px; align-content: start; font-size: 14px;">
|
||||
<div style="font-size: 13px; color: var(--ink-3); margin-bottom: 2px;">Документы</div>
|
||||
<a href="#top" style="color: var(--ink-2);" style-hover="color: var(--ink);">Условия использования</a>
|
||||
<a href="#top" style="color: var(--ink-2);" style-hover="color: var(--ink);">Политика конфиденциальности</a>
|
||||
<a href="#top" style="color: var(--ink-2);" style-hover="color: var(--ink);">Возврат средств</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="max-width: 1200px; margin: 0 auto; padding: 0 clamp(16px, 4vw, 24px) 40px; display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 14px;">
|
||||
<div style="font-size: 13px; color: var(--ink-3);">© 2026 Вейликс</div>
|
||||
<div style="display: flex; align-items: center; gap: 8px;">
|
||||
<div style="display: flex; padding: 2px; border-radius: 9px; border: 1px solid var(--border); background: var(--surface); flex: none;">
|
||||
<button type="button" onClick="{{ setLight }}" title="Светлая тема" style="display: flex; align-items: center; justify-content: center; width: 28px; height: 28px; border: 0; border-radius: 7px; background: {{ lightBg }}; color: {{ lightInk }}; cursor: pointer;"><span style="position: relative; width: 14px; height: 14px; display: block;"><span style="position: absolute; left: 3px; top: 3px; width: 8px; height: 8px; border-radius: 50%; background: currentColor; display: block;"></span><span style="position: absolute; left: 0; top: 0; width: 14px; height: 14px; border-radius: 50%; border: 1.5px solid currentColor; opacity: 0.45; display: block;"></span></span></button>
|
||||
<button type="button" onClick="{{ setDark }}" title="Тёмная тема" style="display: flex; align-items: center; justify-content: center; width: 28px; height: 28px; border: 0; border-radius: 7px; background: {{ darkBg }}; color: {{ darkInk }}; cursor: pointer;"><span style="width: 14px; height: 14px; border-radius: 50%; box-shadow: inset -5px -1.5px 0 0 currentColor; display: block;"></span></button>
|
||||
<button type="button" onClick="{{ setSystem }}" title="Как в системе" style="display: flex; align-items: center; justify-content: center; width: 28px; height: 28px; border: 0; border-radius: 7px; background: {{ systemBg }}; color: {{ systemInk }}; cursor: pointer;"><span style="width: 14px; height: 14px; border-radius: 50%; border: 1.5px solid currentColor; background: linear-gradient(90deg, currentColor 50%, transparent 50%); display: block;"></span></button>
|
||||
</div>
|
||||
<div style="display: flex; padding: 2px; border-radius: 9px; border: 1px solid var(--border); background: var(--surface); flex: none;">
|
||||
<button type="button" onClick="{{ setRu }}" style="padding: 0 10px; height: 28px; border: 0; border-radius: 7px; background: {{ ruBg }}; color: {{ ruInk }}; font-family: inherit; font-size: 12px; cursor: pointer;">RU</button>
|
||||
<button type="button" onClick="{{ setEn }}" style="padding: 0 10px; height: 28px; border: 0; border-radius: 7px; background: {{ enBg }}; color: {{ enInk }}; font-family: inherit; font-size: 12px; cursor: pointer;">EN</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</x-dc>
|
||||
<script type="text/x-dc" data-dc-script data-props="{"defaultTheme":{"editor":"enum","options":["light","dark","system"],"default":"light","tsType":"'light' | 'dark' | 'system'","section":"Тема"}}">
|
||||
class Component extends DCLogic {
|
||||
state = { theme: null, sysDark: false, loc: 0, lead: false, leadSent: false, lang: "ru", narrow: false, menu: false, langMenu: false };
|
||||
|
||||
data() {
|
||||
const base = { setup: "4 900 ₽", month: "2 499 ₽" };
|
||||
const rare = { setup: "6 900 ₽", month: "3 499 ₽" };
|
||||
const far = { setup: "9 900 ₽", month: "4 999 ₽" };
|
||||
const L = (country, city, code, lng, lat, ready, tier) => ({ country, city, code, lng, lat, ready, setup: tier.setup, month: tier.month, rare: tier !== base });
|
||||
return [
|
||||
L("Нидерланды", "Амстердам", "nl", 4.9, 52.37, true, base),
|
||||
L("Германия", "Франкфурт", "de", 8.68, 50.11, true, base),
|
||||
L("США", "Нью-Йорк", "us", -74.01, 40.71, true, base),
|
||||
L("Великобритания", "Лондон", "gb", -0.13, 51.51, true, base),
|
||||
L("Франция", "Париж", "fr", 2.35, 48.86, true, base),
|
||||
L("Финляндия", "Хельсинки", "fi", 24.94, 60.17, true, base),
|
||||
L("Швеция", "Стокгольм", "se", 18.07, 59.33, true, base),
|
||||
L("Норвегия", "Осло", "no", 10.75, 59.91, false, base),
|
||||
L("Дания", "Копенгаген", "dk", 12.57, 55.68, false, base),
|
||||
L("Швейцария", "Цюрих", "ch", 8.54, 47.37, true, rare),
|
||||
L("Австрия", "Вена", "at", 16.37, 48.21, true, base),
|
||||
L("Испания", "Мадрид", "es", -3.7, 40.42, true, base),
|
||||
L("Италия", "Милан", "it", 9.19, 45.46, true, base),
|
||||
L("Польша", "Варшава", "pl", 21.01, 52.23, true, base),
|
||||
L("Чехия", "Прага", "cz", 14.44, 50.08, true, base),
|
||||
L("Румыния", "Бухарест", "ro", 26.1, 44.43, false, base),
|
||||
L("Литва", "Вильнюс", "lt", 25.28, 54.69, true, base),
|
||||
L("Латвия", "Рига", "lv", 24.11, 56.95, true, base),
|
||||
L("Эстония", "Таллин", "ee", 24.75, 59.44, false, base),
|
||||
L("Турция", "Стамбул", "tr", 28.98, 41.01, true, base),
|
||||
L("Казахстан", "Алматы", "kz", 76.89, 43.24, false, base),
|
||||
L("Армения", "Ереван", "am", 44.51, 40.18, false, base),
|
||||
L("Грузия", "Тбилиси", "ge", 44.8, 41.72, false, base),
|
||||
L("ОАЭ", "Дубай", "ae", 55.27, 25.2, true, rare),
|
||||
L("Сингапур", "Сингапур", "sg", 103.82, 1.35, true, rare),
|
||||
L("Япония", "Токио", "jp", 139.69, 35.69, true, rare),
|
||||
L("Южная Корея", "Сеул", "kr", 126.98, 37.57, false, rare),
|
||||
L("Канада", "Торонто", "ca", -79.38, 43.65, true, base),
|
||||
L("Австралия", "Сидней", "au", 151.21, -33.87, false, far),
|
||||
L("Бразилия", "Сан-Паулу", "br", -46.63, -23.55, false, far)
|
||||
];
|
||||
}
|
||||
|
||||
flagSpecs() {
|
||||
return {
|
||||
nl: { rows: ["#AE1C28", "#FFFFFF", "#21468B"] },
|
||||
de: { rows: ["#000000", "#DD0000", "#FFCE00"] },
|
||||
us: { rows: ["#B22234", "#FFFFFF", "#B22234", "#FFFFFF", "#B22234", "#FFFFFF"], marks: [{ l: 0, t: 0, w: 22, h: 17, bg: "#3C3B6E" }] },
|
||||
gb: { bg: "#012169", marks: [{ l: 17, t: 0, w: 12, h: 32, bg: "#FFFFFF" }, { l: 0, t: 10, w: 46, h: 12, bg: "#FFFFFF" }, { l: 20, t: 0, w: 6, h: 32, bg: "#C8102E" }, { l: 0, t: 13, w: 46, h: 6, bg: "#C8102E" }] },
|
||||
fr: { cols: ["#002395", "#FFFFFF", "#ED2939"] },
|
||||
fi: { bg: "#FFFFFF", marks: [{ l: 13, t: 0, w: 8, h: 32, bg: "#003580" }, { l: 0, t: 12, w: 46, h: 8, bg: "#003580" }] },
|
||||
se: { bg: "#006AA7", marks: [{ l: 13, t: 0, w: 6, h: 32, bg: "#FECC00" }, { l: 0, t: 13, w: 46, h: 6, bg: "#FECC00" }] },
|
||||
no: { bg: "#BA0C2F", marks: [{ l: 12, t: 0, w: 10, h: 32, bg: "#FFFFFF" }, { l: 0, t: 11, w: 46, h: 10, bg: "#FFFFFF" }, { l: 15, t: 0, w: 4, h: 32, bg: "#00205B" }, { l: 0, t: 14, w: 46, h: 4, bg: "#00205B" }] },
|
||||
dk: { bg: "#C8102E", marks: [{ l: 13, t: 0, w: 6, h: 32, bg: "#FFFFFF" }, { l: 0, t: 13, w: 46, h: 6, bg: "#FFFFFF" }] },
|
||||
ch: { bg: "#D52B1E", marks: [{ l: 20, t: 8, w: 6, h: 16, bg: "#FFFFFF" }, { l: 15, t: 13, w: 16, h: 6, bg: "#FFFFFF" }] },
|
||||
at: { rows: ["#ED2939", "#FFFFFF", "#ED2939"] },
|
||||
es: { rows: ["#AA151B", "#F1BF00", "#AA151B"], sizes: "1fr 2fr 1fr" },
|
||||
it: { cols: ["#008C45", "#F4F5F0", "#CD212A"] },
|
||||
pl: { rows: ["#FFFFFF", "#DC143C"] },
|
||||
cz: { rows: ["#FFFFFF", "#D7141A"], marks: [{ l: 0, t: 0, w: 18, h: 32, bg: "#11457E", clip: "polygon(0 0, 100% 50%, 0 100%)" }] },
|
||||
ro: { cols: ["#002B7F", "#FCD116", "#CE1126"] },
|
||||
lt: { rows: ["#FDB913", "#006A44", "#C1272D"] },
|
||||
lv: { rows: ["#9E3039", "#FFFFFF", "#9E3039"], sizes: "2fr 1fr 2fr" },
|
||||
ee: { rows: ["#0072CE", "#000000", "#FFFFFF"] },
|
||||
tr: { bg: "#E30A17", marks: [{ l: 11, t: 8, w: 16, h: 16, r: "50%", bg: "#FFFFFF" }, { l: 16, t: 8, w: 16, h: 16, r: "50%", bg: "#E30A17" }] },
|
||||
kz: { bg: "#00AFCA", marks: [{ l: 16, t: 8, w: 15, h: 15, r: "50%", bg: "#FEC50C" }] },
|
||||
am: { rows: ["#D90012", "#0033A0", "#F2A800"] },
|
||||
ge: { bg: "#FFFFFF", marks: [{ l: 20, t: 0, w: 6, h: 32, bg: "#FF0000" }, { l: 0, t: 13, w: 46, h: 6, bg: "#FF0000" }] },
|
||||
ae: { rows: ["#00732F", "#FFFFFF", "#000000"], marks: [{ l: 0, t: 0, w: 11, h: 32, bg: "#FF0000" }] },
|
||||
sg: { rows: ["#EF3340", "#FFFFFF"], marks: [{ l: 5, t: 3, w: 11, h: 11, r: "50%", bg: "#FFFFFF" }, { l: 9, t: 3, w: 11, h: 11, r: "50%", bg: "#EF3340" }] },
|
||||
jp: { bg: "#FFFFFF", marks: [{ l: 16, t: 9, w: 14, h: 14, r: "50%", bg: "#BC002D" }] },
|
||||
kr: { bg: "#FFFFFF", marks: [{ l: 16, t: 9, w: 14, h: 14, r: "50%", bg: "#CD2E3A" }, { l: 16, t: 9, w: 14, h: 14, r: "50%", bg: "#0047A0", clip: "polygon(0 50%, 100% 50%, 100% 100%, 0 100%)" }] },
|
||||
ca: { cols: ["#D80621", "#FFFFFF", "#D80621"], sizes: "1fr 2fr 1fr", marks: [{ l: 20, t: 9, w: 7, h: 13, bg: "#D80621", clip: "polygon(50% 0, 100% 100%, 0 100%)" }] },
|
||||
au: { bg: "#00247D", marks: [{ l: 9, t: 0, w: 4, h: 16, bg: "#FFFFFF" }, { l: 0, t: 6, w: 22, h: 4, bg: "#FFFFFF" }, { l: 30, t: 19, w: 7, h: 7, r: "50%", bg: "#FFFFFF" }] },
|
||||
br: { bg: "#009739", marks: [{ l: 9, t: 4, w: 28, h: 24, bg: "#FEDD00", clip: "polygon(50% 0, 100% 50%, 50% 100%, 0 50%)" }, { l: 18, t: 11, w: 10, h: 10, r: "50%", bg: "#012169" }] }
|
||||
};
|
||||
}
|
||||
|
||||
flag(code) {
|
||||
const h = React.createElement;
|
||||
const spec = this.flagSpecs()[code] || {};
|
||||
const style = {
|
||||
width: "46px", height: "32px", borderRadius: "6px", overflow: "hidden",
|
||||
display: "grid", border: "1px solid var(--border)", position: "relative", flex: "none"
|
||||
};
|
||||
let bands = [];
|
||||
if (spec.rows) {
|
||||
bands = spec.rows;
|
||||
style.gridTemplateRows = spec.sizes || "repeat(" + bands.length + ", 1fr)";
|
||||
} else if (spec.cols) {
|
||||
bands = spec.cols;
|
||||
style.gridAutoFlow = "column";
|
||||
style.gridTemplateColumns = spec.sizes || "repeat(" + bands.length + ", 1fr)";
|
||||
} else {
|
||||
style.background = spec.bg || "var(--surface-2)";
|
||||
}
|
||||
const kids = bands.map((c, i) => h("span", { key: "b" + i, style: { background: c, display: "block" } }));
|
||||
(spec.marks || []).forEach((m, i) => kids.push(h("span", {
|
||||
key: "m" + i,
|
||||
style: {
|
||||
position: "absolute", left: m.l + "px", top: m.t + "px", width: m.w + "px", height: m.h + "px",
|
||||
borderRadius: m.r || 0, background: m.bg, clipPath: m.clip || "none", display: "block"
|
||||
}
|
||||
})));
|
||||
return h("span", { style: style }, kids);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const mq = window.matchMedia("(prefers-color-scheme: dark)");
|
||||
this.mq = mq;
|
||||
this.onSys = () => this.setState({ sysDark: mq.matches });
|
||||
mq.addEventListener("change", this.onSys);
|
||||
this.onResize = () => {
|
||||
const n = window.innerWidth < 900;
|
||||
if (n !== this.state.narrow) this.setState({ narrow: n, menu: false });
|
||||
};
|
||||
window.addEventListener("resize", this.onResize);
|
||||
this.onResize();
|
||||
let saved = null;
|
||||
try { saved = localStorage.getItem("veilix.theme"); } catch (e) {}
|
||||
let savedLang = null;
|
||||
try { savedLang = localStorage.getItem("veilix.lang"); } catch (e) {}
|
||||
this.setState({ sysDark: mq.matches, theme: saved || this.props.defaultTheme || "light", lang: savedLang || "ru" }, () => { this.apply(); this.applyLang(); });
|
||||
}
|
||||
|
||||
applyLang() {
|
||||
const lang = this.state.lang || "ru";
|
||||
if (window.VEILIX_APPLY_LANG) window.VEILIX_APPLY_LANG(document.body, lang);
|
||||
else setTimeout(() => this.applyLang(), 150);
|
||||
}
|
||||
|
||||
setLang(l) {
|
||||
try { localStorage.setItem("veilix.lang", l); } catch (e) {}
|
||||
this.setState({ lang: l, langMenu: false }, () => this.applyLang());
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.mq) this.mq.removeEventListener("change", this.onSys);
|
||||
if (this.onResize) window.removeEventListener("resize", this.onResize);
|
||||
}
|
||||
componentDidUpdate() { this.apply(); this.applyLang(); }
|
||||
|
||||
apply() {
|
||||
const theme = this.state.theme || this.props.defaultTheme || "light";
|
||||
const dark = theme === "dark" || (theme === "system" && this.state.sysDark);
|
||||
document.documentElement.setAttribute("data-veilix-theme", dark ? "dark" : "light");
|
||||
}
|
||||
|
||||
setTheme(t) {
|
||||
try { localStorage.setItem("veilix.theme", t); } catch (e) {}
|
||||
this.setState({ theme: t }, () => this.apply());
|
||||
}
|
||||
|
||||
renderVals() {
|
||||
const theme = this.state.theme || this.props.defaultTheme || "light";
|
||||
const sel = "var(--accent-soft)";
|
||||
const names = ["Нидерланды · Амстердам", "Германия · Франкфурт", "США · Нью-Йорк", "Финляндия · Хельсинки", "Турция · Стамбул", "Япония · Токио"];
|
||||
const ready = [true, true, true, false, false, false];
|
||||
const i = this.state.loc;
|
||||
const v = {
|
||||
items: this.data().map((d, n) => ({
|
||||
country: d.country,
|
||||
city: d.city,
|
||||
flag: this.flag(d.code),
|
||||
status: d.ready ? "Серверы в наличии" : "Установим за 24 часа",
|
||||
statusInk: d.ready ? "var(--accent)" : "var(--ink-3)",
|
||||
statusDot: d.ready ? "var(--accent)" : "transparent",
|
||||
statusRing: d.ready ? "0" : "1.5px solid var(--ink-3)",
|
||||
bg: this.state.loc === n ? sel : "var(--surface)",
|
||||
border: this.state.loc === n ? "var(--accent)" : "var(--border)",
|
||||
dot: this.state.loc === n ? "var(--accent)" : "transparent",
|
||||
dotOff: this.state.loc === n,
|
||||
buy: (e) => { e.stopPropagation(); window.location.href = "Order.dc.html"; },
|
||||
setup: d.setup,
|
||||
month: d.month,
|
||||
buyOff: this.state.loc !== n,
|
||||
pick: () => this.setState({ loc: n })
|
||||
})),
|
||||
pickedTitle: names[i],
|
||||
pickedHint: ready[i] ? "Сервер свободен — доступ выдаём сразу после оплаты" : "Свободного сервера нет: установим лично под вас, доступ в течение 24 часов",
|
||||
openLead: () => this.setState({ lead: true }),
|
||||
navOff: this.state.narrow,
|
||||
burgerOff: !this.state.narrow,
|
||||
menuOff: !(this.state.narrow && this.state.menu),
|
||||
toggleMenu: () => this.setState(st => ({ menu: !st.menu })),
|
||||
closeMenu: () => this.setState({ menu: false }),
|
||||
toggleLangMenu: () => this.setState(st => ({ langMenu: !st.langMenu })),
|
||||
langMenuOff: !this.state.langMenu,
|
||||
langLabel: this.state.lang === "en" ? "EN" : "RU",
|
||||
ruCheck: this.state.lang === "en" ? "" : "✓",
|
||||
enCheck: this.state.lang === "en" ? "✓" : "",
|
||||
setRu: () => this.setLang("ru"),
|
||||
setEn: () => this.setLang("en"),
|
||||
ruBg: this.state.lang === "en" ? "transparent" : sel,
|
||||
enBg: this.state.lang === "en" ? sel : "transparent",
|
||||
ruInk: this.state.lang === "en" ? "var(--ink-3)" : "var(--ink)",
|
||||
enInk: this.state.lang === "en" ? "var(--ink)" : "var(--ink-3)",
|
||||
sendLead: () => this.setState({ leadSent: true }),
|
||||
leadOpen: this.state.lead,
|
||||
leadClosed: !this.state.lead,
|
||||
leadButton: this.state.leadSent ? "Заявка отправлена" : "Отправить",
|
||||
setLight: () => this.setTheme("light"),
|
||||
setDark: () => this.setTheme("dark"),
|
||||
setSystem: () => this.setTheme("system"),
|
||||
lightBg: theme === "light" ? sel : "transparent",
|
||||
darkBg: theme === "dark" ? sel : "transparent",
|
||||
systemBg: theme === "system" ? sel : "transparent",
|
||||
lightInk: theme === "light" ? "var(--ink)" : "var(--ink-3)",
|
||||
darkInk: theme === "dark" ? "var(--ink)" : "var(--ink-3)",
|
||||
systemInk: theme === "system" ? "var(--ink)" : "var(--ink-3)"
|
||||
};
|
||||
for (let n = 0; n < 6; n++) {
|
||||
v["loc" + n + "Bg"] = this.state.loc === n ? sel : "var(--surface)";
|
||||
v["loc" + n + "Border"] = this.state.loc === n ? "var(--accent)" : "var(--border)";
|
||||
v["loc" + n + "Dot"] = this.state.loc === n ? "var(--accent)" : "transparent";
|
||||
v["pickLoc" + n] = () => this.setState({ loc: n });
|
||||
}
|
||||
return v;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,367 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<script src="./support.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<x-dc>
|
||||
<helmet>
|
||||
<link rel="icon" type="image/svg+xml" href="favicon.svg" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin="anonymous" />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Commissioner:wght@300;400;500;600&family=Unbounded:wght@500;600&display=swap" rel="stylesheet" />
|
||||
<script>(function(){if(window.__veilixI18nSrc)return;window.__veilixI18nSrc="./i18n.js?v="+Date.now().toString(36)+Math.random().toString(36).slice(2,8);var s=document.createElement("script");s.src=window.__veilixI18nSrc;document.head.appendChild(s);})();</script>
|
||||
<style>
|
||||
:root{
|
||||
--bg: oklch(99% 0.005 296);
|
||||
--surface: oklch(100% 0 0);
|
||||
--surface-2: oklch(97.5% 0.01 296);
|
||||
--ink: oklch(24% 0.04 296);
|
||||
--ink-2: oklch(45% 0.03 296);
|
||||
--ink-3: oklch(60% 0.025 296);
|
||||
--border: oklch(92% 0.014 296);
|
||||
--accent: oklch(45% 0.17 296);
|
||||
--accent-hover: oklch(38% 0.17 296);
|
||||
--accent-soft: oklch(95.5% 0.028 296);
|
||||
--on-accent: oklch(99% 0.01 296);
|
||||
--glow-1: oklch(86% 0.1 296);
|
||||
--glow-2: oklch(89% 0.09 266);
|
||||
--glow-3: oklch(91% 0.08 332);
|
||||
--veil: oklch(100% 0 0 / 0.55);
|
||||
--shadow: 0 1px 2px oklch(24% 0.04 296 / 0.05), 0 12px 32px -12px oklch(24% 0.04 296 / 0.14);
|
||||
}
|
||||
:root[data-veilix-theme="dark"]{
|
||||
--bg: oklch(17% 0.022 296);
|
||||
--surface: oklch(21% 0.026 296);
|
||||
--surface-2: oklch(24% 0.03 296);
|
||||
--ink: oklch(96% 0.01 296);
|
||||
--ink-2: oklch(79% 0.02 296);
|
||||
--ink-3: oklch(63% 0.025 296);
|
||||
--border: oklch(31% 0.032 296);
|
||||
--accent: oklch(74% 0.15 296);
|
||||
--accent-hover: oklch(80% 0.13 296);
|
||||
--accent-soft: oklch(27% 0.05 296);
|
||||
--on-accent: oklch(17% 0.03 296);
|
||||
--glow-1: oklch(45% 0.16 296);
|
||||
--glow-2: oklch(40% 0.14 266);
|
||||
--glow-3: oklch(42% 0.13 332);
|
||||
--veil: oklch(100% 0 0 / 0.07);
|
||||
--shadow: 0 1px 2px oklch(10% 0.02 296 / 0.4), 0 16px 40px -16px oklch(8% 0.02 296 / 0.7);
|
||||
}
|
||||
*{ box-sizing: border-box; }
|
||||
input, textarea, select{ font-size: 16px; }
|
||||
[hidden]{ display: none !important; }
|
||||
body{ margin:0; background: var(--bg); color: var(--ink); font-family:"Commissioner", system-ui, sans-serif; -webkit-font-smoothing: antialiased; }
|
||||
a{ color: var(--accent); text-decoration: none; }
|
||||
a:hover{ color: var(--accent-hover); }
|
||||
</style>
|
||||
</helmet>
|
||||
|
||||
<template id="__bundler_thumbnail" data-bg-color="#efeaf8">
|
||||
<svg viewBox="0 0 1200 800" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="480" y="290" width="150" height="150" rx="48" fill="#5b32c9" />
|
||||
<rect x="570" y="360" width="150" height="150" rx="48" fill="#ffffff" fill-opacity="0.62" stroke="#5b32c9" stroke-width="6" />
|
||||
</svg>
|
||||
</template>
|
||||
<div style="background: var(--bg); color: var(--ink); font-family: 'Commissioner', system-ui, sans-serif; font-weight: 400; letter-spacing: -0.01em; min-height: 100vh;">
|
||||
|
||||
<header style="position: sticky; top: 0; z-index: 40; background: color-mix(in oklab, var(--bg) 82%, transparent); backdrop-filter: blur(20px) saturate(1.4); -webkit-backdrop-filter: blur(20px) saturate(1.4); border-bottom: 1px solid var(--border);">
|
||||
<div style="max-width: 1200px; margin: 0 auto; padding: 14px 24px; min-height: 66px; display: flex; flex-wrap: nowrap; align-items: center; gap: 14px 40px;">
|
||||
<a href="Main.dc.html" style="display: flex; align-items: center; gap: 10px; color: var(--ink); flex: none;">
|
||||
<span style="position: relative; width: 28px; height: 28px; display: block; flex: none;">
|
||||
<span style="position: absolute; left: 0; top: 3px; width: 21px; height: 21px; border-radius: 7px; background: linear-gradient(140deg, var(--accent), color-mix(in oklab, var(--accent) 55%, var(--glow-3))); display: block;"></span>
|
||||
<span style="position: absolute; right: 0; bottom: 3px; width: 21px; height: 21px; border-radius: 7px; background: var(--veil); border: 1px solid color-mix(in oklab, var(--accent) 35%, transparent); backdrop-filter: blur(2px); display: block;"></span>
|
||||
</span>
|
||||
<span style="font-family: 'Unbounded', 'Commissioner', sans-serif; font-weight: 600; font-size: 17px; letter-spacing: -0.02em; width: 88px; flex: none; white-space: nowrap;">Вейликс</span>
|
||||
</a>
|
||||
<nav hidden="{{ navOff }}" style="display: flex; flex-wrap: wrap; gap: 10px 28px; font-size: 15px; font-weight: 500; color: var(--ink-2);">
|
||||
<a href="Price.dc.html" style="color: var(--ink-2);" style-hover="color: var(--ink);">Цены</a>
|
||||
<a href="Locations.dc.html" style="color: var(--ink-2);" style-hover="color: var(--ink);">Локации</a>
|
||||
<a href="#top" style="color: var(--ink);">Поддержка</a>
|
||||
</nav>
|
||||
<div style="margin-left: auto; display: flex; flex-wrap: wrap; align-items: center; gap: 8px;">
|
||||
<div hidden="{{ navOff }}" style="position: relative;">
|
||||
<button type="button" onClick="{{ toggleLangMenu }}" title="Язык" style="display: flex; align-items: center; gap: 7px; height: 38px; padding: 0 11px; border: 1px solid var(--border); border-radius: 10px; background: transparent; color: var(--ink-2); font-family: inherit; font-size: 14px; cursor: pointer;" style-hover="color: var(--ink); border-color: var(--ink-3);">
|
||||
<span style="position: relative; width: 16px; height: 16px; border-radius: 50%; border: 1.5px solid currentColor; display: block; flex: none;">
|
||||
<span style="position: absolute; left: -1.5px; top: 5.75px; width: 16px; height: 1.5px; background: currentColor; display: block;"></span>
|
||||
<span style="position: absolute; left: 3.25px; top: -1.5px; width: 8px; height: 16px; border-radius: 50%; border: 1.5px solid currentColor; display: block;"></span>
|
||||
</span>
|
||||
<span style="font-variant-numeric: tabular-nums;">{{ langLabel }}</span>
|
||||
</button>
|
||||
<div hidden="{{ langMenuOff }}" style="position: absolute; right: 0; top: 46px; width: 172px; padding: 6px; background: var(--surface); border: 1px solid var(--border); border-radius: 14px; box-shadow: var(--shadow); font-size: 14px;">
|
||||
<button type="button" onClick="{{ setRu }}" style="display: flex; align-items: center; justify-content: space-between; gap: 12px; width: 100%; padding: 9px 10px; border: 0; border-radius: 9px; background: {{ ruBg }}; color: var(--ink); font-family: inherit; font-size: 14px; text-align: left; cursor: pointer;">
|
||||
<span>Русский</span>
|
||||
<span style="color: var(--accent);">{{ ruCheck }}</span>
|
||||
</button>
|
||||
<button type="button" onClick="{{ setEn }}" style="display: flex; align-items: center; justify-content: space-between; gap: 12px; width: 100%; padding: 9px 10px; border: 0; border-radius: 9px; background: {{ enBg }}; color: var(--ink); font-family: inherit; font-size: 14px; text-align: left; cursor: pointer;">
|
||||
<span>English</span>
|
||||
<span style="color: var(--accent);">{{ enCheck }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<a href="App.dc.html" hidden="{{ navOff }}" style="display: flex; align-items: center; justify-content: center; height: 38px; width: 78px; flex: none; box-sizing: border-box; border-radius: 10px; color: var(--ink-2); font-size: 15px;" style-hover="color: var(--ink); background: var(--surface-2);">Войти</a>
|
||||
<a href="Price.dc.html" hidden="{{ navOff }}" style="display: flex; align-items: center; justify-content: center; height: 38px; width: 142px; flex: none; box-sizing: border-box; border-radius: 10px; background: var(--accent); color: var(--on-accent); font-size: 15px; font-weight: 500; box-shadow: 0 6px 18px -8px var(--accent);" style-hover="background: var(--accent-hover); color: var(--on-accent);">Получить VPN</a>
|
||||
<button type="button" hidden="{{ burgerOff }}" onClick="{{ toggleMenu }}" title="Меню" style="display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 5px; width: 42px; height: 38px; border: 1px solid var(--border); border-radius: 10px; background: transparent; color: var(--ink); cursor: pointer;" style-hover="border-color: var(--ink-3);">
|
||||
<span style="width: 18px; height: 2px; border-radius: 2px; background: currentColor; display: block;"></span>
|
||||
<span style="width: 18px; height: 2px; border-radius: 2px; background: currentColor; display: block;"></span>
|
||||
<span style="width: 18px; height: 2px; border-radius: 2px; background: currentColor; display: block;"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div hidden="{{ menuOff }}" style="position: absolute; left: 0; right: 0; top: 100%; z-index: 45; padding: 10px 20px 18px; background: var(--bg); border-bottom: 1px solid var(--border); box-shadow: var(--shadow); display: grid; gap: 2px;">
|
||||
<a href="Price.dc.html" onClick="{{ closeMenu }}" style="padding: 13px 6px; font-size: 17px; color: var(--ink);">Цены</a>
|
||||
<a href="Locations.dc.html" onClick="{{ closeMenu }}" style="padding: 13px 6px; font-size: 17px; color: var(--ink);">Локации</a>
|
||||
<a href="Support.dc.html" onClick="{{ closeMenu }}" style="padding: 13px 6px; font-size: 17px; color: var(--ink);">Поддержка</a>
|
||||
<div style="display: flex; align-items: center; justify-content: space-between; gap: 12px; padding-top: 6px; margin-top: 6px; border-top: 1px solid var(--border);">
|
||||
<a href="App.dc.html" onClick="{{ closeMenu }}" style="padding: 13px 6px; font-size: 17px; color: var(--ink);">Войти</a>
|
||||
<div style="display: flex; gap: 6px; flex: none;">
|
||||
<button type="button" onClick="{{ setRu }}" style="padding: 9px 13px; border: 1px solid var(--border); border-radius: 10px; background: {{ ruBg }}; color: var(--ink); font-family: inherit; font-size: 13px; cursor: pointer; white-space: nowrap;">Русский</button>
|
||||
<button type="button" onClick="{{ setEn }}" style="padding: 9px 13px; border: 1px solid var(--border); border-radius: 10px; background: {{ enBg }}; color: var(--ink); font-family: inherit; font-size: 13px; cursor: pointer; white-space: nowrap;">English</button>
|
||||
</div>
|
||||
</div>
|
||||
<a href="Price.dc.html" onClick="{{ closeMenu }}" style="display: flex; align-items: center; justify-content: center; height: 50px; margin-top: 12px; border-radius: 12px; background: var(--accent); color: var(--on-accent); font-size: 16px; font-weight: 500;">Получить VPN</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main id="top" style="max-width: 1000px; margin: 0 auto; padding: 0 24px 80px;">
|
||||
|
||||
<section style="padding: 56px 0 8px;">
|
||||
<h1 style="margin: 0 0 14px; font-size: clamp(34px, 4.4vw, 52px); font-weight: 600; letter-spacing: -0.035em;">Поддержка</h1>
|
||||
<div style="height: 32px;"></div>
|
||||
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 16px;">
|
||||
<a href="#form" style="display: grid; gap: 10px; padding: clamp(20px, 4.8vw, 28px); border-radius: 22px; background: var(--surface); border: 1px solid var(--border); box-shadow: var(--shadow); color: var(--ink);" style-hover="border-color: var(--accent); color: var(--ink);">
|
||||
<span style="font-size: 13px; color: var(--ink-3);">Telegram</span>
|
||||
<span style="font-size: clamp(24px, 3vw, 32px); font-weight: 600; letter-spacing: -0.03em; color: var(--accent);">@veilix_support</span>
|
||||
<span style="font-size: 14px; color: var(--ink-2); font-weight: 300;">Быстрее всего. Можно писать голосом и скидывать скриншоты.</span>
|
||||
</a>
|
||||
<a href="#form" style="display: grid; gap: 10px; padding: clamp(20px, 4.8vw, 28px); border-radius: 22px; background: var(--surface); border: 1px solid var(--border); box-shadow: var(--shadow); color: var(--ink);" style-hover="border-color: var(--accent); color: var(--ink);">
|
||||
<span style="font-size: 13px; color: var(--ink-3);">Почта</span>
|
||||
<span style="font-size: clamp(22px, 2.6vw, 30px); font-weight: 600; letter-spacing: -0.03em; color: var(--accent); word-break: break-word;">help@veilix.cc</span>
|
||||
<span style="font-size: 14px; color: var(--ink-2); font-weight: 300;">Для счетов, возвратов и всего, что нужно письменно.</span>
|
||||
</a>
|
||||
</div>
|
||||
<p style="margin: 16px 0 0; font-size: 14px; color: var(--ink-3);">Отвечаем с 10 до 20 в рабочие дни.</p>
|
||||
</section>
|
||||
|
||||
<section style="padding: 64px 0 0;">
|
||||
<h2 style="margin: 0 0 20px; font-size: clamp(26px, 2.8vw, 34px); font-weight: 600; letter-spacing: -0.03em;">Частые вопросы</h2>
|
||||
|
||||
<div style="display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 22px;">
|
||||
<button type="button" onClick="{{ tab0 }}" style="padding: 9px 16px; border-radius: 10px; border: 1px solid {{ tab0Border }}; background: {{ tab0Bg }}; color: {{ tab0Ink }}; font-family: inherit; font-size: 14px; cursor: pointer;">Подключение</button>
|
||||
<button type="button" onClick="{{ tab1 }}" style="padding: 9px 16px; border-radius: 10px; border: 1px solid {{ tab1Border }}; background: {{ tab1Bg }}; color: {{ tab1Ink }}; font-family: inherit; font-size: 14px; cursor: pointer;">Оплата и продление</button>
|
||||
<button type="button" onClick="{{ tab2 }}" style="padding: 9px 16px; border-radius: 10px; border: 1px solid {{ tab2Border }}; background: {{ tab2Bg }}; color: {{ tab2Ink }}; font-family: inherit; font-size: 14px; cursor: pointer;">Локации и IP</button>
|
||||
</div>
|
||||
|
||||
<div style="display: grid; gap: 10px;">
|
||||
<sc-for list="{{ items }}" as="item" hint-placeholder-count="4">
|
||||
<div style="border: 1px solid var(--border); border-radius: 16px; background: var(--surface); overflow: hidden;">
|
||||
<button type="button" onClick="{{ item.toggle }}" style="display: flex; align-items: center; gap: 16px; width: 100%; padding: 20px 22px; background: transparent; border: 0; color: var(--ink); font-family: inherit; font-size: 17px; font-weight: 500; text-align: left; cursor: pointer; letter-spacing: -0.015em;">
|
||||
<span style="flex: 1;">{{ item.q }}</span>
|
||||
<span style="flex: none; width: 10px; height: 10px; border-right: 1.5px solid var(--ink-3); border-bottom: 1.5px solid var(--ink-3); transform: {{ item.rot }}; transition: transform 180ms ease; display: block;"></span>
|
||||
</button>
|
||||
<div hidden="{{ item.off }}" style="padding: 0 22px 22px; font-size: 15px; line-height: 1.6; color: var(--ink-2); font-weight: 300; max-width: 62ch;">{{ item.a }}</div>
|
||||
</div>
|
||||
</sc-for>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="form" style="padding: 64px 0 0;">
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 40px; padding: 40px; border-radius: 24px; background: var(--surface); border: 1px solid var(--border); box-shadow: var(--shadow);">
|
||||
<div>
|
||||
<h2 style="margin: 0 0 10px; font-size: clamp(24px, 2.6vw, 30px); font-weight: 600; letter-spacing: -0.03em;">Написать нам</h2>
|
||||
<p style="margin: 0; max-width: 26em; font-size: 15px; line-height: 1.55; color: var(--ink-2); font-weight: 300;">Если уже купили ключ, укажите почту, на которую он оформлен — так мы сразу увидим вашу конфигурацию.</p>
|
||||
</div>
|
||||
<div style="display: grid; gap: 10px;">
|
||||
<input type="email" placeholder="Ваша почта" style="height: 48px; padding: 0 14px; border-radius: 12px; border: 1px solid var(--border); background: var(--bg); color: var(--ink); font-family: inherit; font-size: 15px; width: 100%;" />
|
||||
<select style="height: 48px; padding: 0 12px; border-radius: 12px; border: 1px solid var(--border); background: var(--bg); color: var(--ink); font-family: inherit; font-size: 15px; width: 100%;">
|
||||
<option>Тема: подключение</option>
|
||||
<option>Тема: оплата и продление</option>
|
||||
<option>Тема: локации и IP</option>
|
||||
<option>Тема: другое</option>
|
||||
</select>
|
||||
<textarea rows="5" placeholder="Что произошло" style="padding: 12px 14px; border-radius: 12px; border: 1px solid var(--border); background: var(--bg); color: var(--ink); font-family: inherit; font-size: 15px; resize: vertical; width: 100%;"></textarea>
|
||||
<button type="button" onClick="{{ send }}" style="display: inline-flex; align-items: center; justify-content: center; height: 50px; border-radius: 12px; border: 0; background: var(--accent); color: var(--on-accent); font-family: inherit; font-size: 16px; font-weight: 500; cursor: pointer;" style-hover="background: var(--accent-hover);">{{ sendLabel }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer style="border-top: 1px solid var(--border); background: var(--surface-2);">
|
||||
<div style="max-width: 1200px; margin: 0 auto; padding: 48px clamp(16px, 4vw, 24px) 32px; display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 32px;">
|
||||
<div>
|
||||
<div style="display: flex; align-items: center; gap: 10px; margin-bottom: 14px;">
|
||||
<span style="position: relative; width: 24px; height: 24px; display: block; flex: none;">
|
||||
<span style="position: absolute; left: 0; top: 3px; width: 18px; height: 18px; border-radius: 6px; background: linear-gradient(140deg, var(--accent), color-mix(in oklab, var(--accent) 55%, var(--glow-3))); display: block;"></span>
|
||||
<span style="position: absolute; right: 0; bottom: 3px; width: 18px; height: 18px; border-radius: 6px; background: var(--veil); border: 1px solid color-mix(in oklab, var(--accent) 35%, transparent); display: block;"></span>
|
||||
</span>
|
||||
<span style="font-family: 'Unbounded', 'Commissioner', sans-serif; font-weight: 600; font-size: 15px; letter-spacing: -0.02em;">Вейликс</span>
|
||||
</div>
|
||||
<p style="margin: 0 0 20px; max-width: 26em; font-size: 14px; line-height: 1.55; color: var(--ink-3); font-weight: 300;">Персональные VPN-серверы с индивидуальным IP. Работаем с 2022 года.</p>
|
||||
<div style="display: flex; flex-wrap: wrap; gap: 6px;">
|
||||
<span style="display: flex; align-items: center; justify-content: center; width: 60px; height: 28px; border-radius: 8px; border: 1px solid var(--border); background: var(--surface); font-size: 11px; letter-spacing: 0.06em; color: var(--ink-3);">VISA</span>
|
||||
<span style="display: flex; align-items: center; justify-content: center; width: 60px; height: 28px; border-radius: 8px; border: 1px solid var(--border); background: var(--surface); font-size: 11px; letter-spacing: 0.06em; color: var(--ink-3);">MC</span>
|
||||
<span style="display: flex; align-items: center; justify-content: center; width: 60px; height: 28px; border-radius: 8px; border: 1px solid var(--border); background: var(--surface); font-size: 11px; letter-spacing: 0.06em; color: var(--ink-3);">МИР</span>
|
||||
<span style="display: flex; align-items: center; justify-content: center; width: 60px; height: 28px; border-radius: 8px; border: 1px solid var(--border); background: var(--surface); font-size: 11px; letter-spacing: 0.06em; color: var(--ink-3);">CRYPTO</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: grid; grid-template-columns: repeat(2, minmax(130px, 1fr)); gap: 24px 32px; align-content: start;">
|
||||
<div style="display: grid; grid-template-columns: minmax(0, 1fr); gap: 10px; align-content: start; font-size: 14px;">
|
||||
<div style="font-size: 13px; color: var(--ink-3); margin-bottom: 2px;">Сервис</div>
|
||||
<a href="Price.dc.html" style="color: var(--ink-2);" style-hover="color: var(--ink);">Цены</a>
|
||||
<a href="Locations.dc.html" style="color: var(--ink-2);" style-hover="color: var(--ink);">Локации</a>
|
||||
<a href="#top" style="color: var(--ink-2);" style-hover="color: var(--ink);">Поддержка</a>
|
||||
</div>
|
||||
<div style="display: grid; grid-template-columns: minmax(0, 1fr); gap: 10px; align-content: start; font-size: 14px;">
|
||||
<div style="font-size: 13px; color: var(--ink-3); margin-bottom: 2px;">Документы</div>
|
||||
<a href="#top" style="color: var(--ink-2);" style-hover="color: var(--ink);">Условия использования</a>
|
||||
<a href="#top" style="color: var(--ink-2);" style-hover="color: var(--ink);">Политика конфиденциальности</a>
|
||||
<a href="#top" style="color: var(--ink-2);" style-hover="color: var(--ink);">Возврат средств</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="max-width: 1200px; margin: 0 auto; padding: 0 clamp(16px, 4vw, 24px) 40px; display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 14px;">
|
||||
<div style="font-size: 13px; color: var(--ink-3);">© 2026 Вейликс</div>
|
||||
<div style="display: flex; align-items: center; gap: 8px;">
|
||||
<div style="display: flex; padding: 2px; border-radius: 9px; border: 1px solid var(--border); background: var(--surface); flex: none;">
|
||||
<button type="button" onClick="{{ setLight }}" title="Светлая тема" style="display: flex; align-items: center; justify-content: center; width: 28px; height: 28px; border: 0; border-radius: 7px; background: {{ lightBg }}; color: {{ lightInk }}; cursor: pointer;"><span style="position: relative; width: 14px; height: 14px; display: block;"><span style="position: absolute; left: 3px; top: 3px; width: 8px; height: 8px; border-radius: 50%; background: currentColor; display: block;"></span><span style="position: absolute; left: 0; top: 0; width: 14px; height: 14px; border-radius: 50%; border: 1.5px solid currentColor; opacity: 0.45; display: block;"></span></span></button>
|
||||
<button type="button" onClick="{{ setDark }}" title="Тёмная тема" style="display: flex; align-items: center; justify-content: center; width: 28px; height: 28px; border: 0; border-radius: 7px; background: {{ darkBg }}; color: {{ darkInk }}; cursor: pointer;"><span style="width: 14px; height: 14px; border-radius: 50%; box-shadow: inset -5px -1.5px 0 0 currentColor; display: block;"></span></button>
|
||||
<button type="button" onClick="{{ setSystem }}" title="Как в системе" style="display: flex; align-items: center; justify-content: center; width: 28px; height: 28px; border: 0; border-radius: 7px; background: {{ systemBg }}; color: {{ systemInk }}; cursor: pointer;"><span style="width: 14px; height: 14px; border-radius: 50%; border: 1.5px solid currentColor; background: linear-gradient(90deg, currentColor 50%, transparent 50%); display: block;"></span></button>
|
||||
</div>
|
||||
<div style="display: flex; padding: 2px; border-radius: 9px; border: 1px solid var(--border); background: var(--surface); flex: none;">
|
||||
<button type="button" onClick="{{ setRu }}" style="padding: 0 10px; height: 28px; border: 0; border-radius: 7px; background: {{ ruBg }}; color: {{ ruInk }}; font-family: inherit; font-size: 12px; cursor: pointer;">RU</button>
|
||||
<button type="button" onClick="{{ setEn }}" style="padding: 0 10px; height: 28px; border: 0; border-radius: 7px; background: {{ enBg }}; color: {{ enInk }}; font-family: inherit; font-size: 12px; cursor: pointer;">EN</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</x-dc>
|
||||
<script type="text/x-dc" data-dc-script data-props="{"defaultTheme":{"editor":"enum","options":["light","dark","system"],"default":"light","tsType":"'light' | 'dark' | 'system'","section":"Тема"}}">
|
||||
class Component extends DCLogic {
|
||||
state = { theme: null, sysDark: false, tab: 0, open: 0, sent: false, lang: "ru", narrow: false, menu: false, langMenu: false };
|
||||
|
||||
componentDidMount() {
|
||||
const mq = window.matchMedia("(prefers-color-scheme: dark)");
|
||||
this.mq = mq;
|
||||
this.onSys = () => this.setState({ sysDark: mq.matches });
|
||||
mq.addEventListener("change", this.onSys);
|
||||
this.onResize = () => {
|
||||
const n = window.innerWidth < 900;
|
||||
if (n !== this.state.narrow) this.setState({ narrow: n, menu: false });
|
||||
};
|
||||
window.addEventListener("resize", this.onResize);
|
||||
this.onResize();
|
||||
let saved = null;
|
||||
try { saved = localStorage.getItem("veilix.theme"); } catch (e) {}
|
||||
let savedLang = null;
|
||||
try { savedLang = localStorage.getItem("veilix.lang"); } catch (e) {}
|
||||
this.setState({ sysDark: mq.matches, theme: saved || this.props.defaultTheme || "light", lang: savedLang || "ru" }, () => { this.apply(); this.applyLang(); });
|
||||
}
|
||||
|
||||
applyLang() {
|
||||
const lang = this.state.lang || "ru";
|
||||
if (window.VEILIX_APPLY_LANG) window.VEILIX_APPLY_LANG(document.body, lang);
|
||||
else setTimeout(() => this.applyLang(), 150);
|
||||
}
|
||||
|
||||
setLang(l) {
|
||||
try { localStorage.setItem("veilix.lang", l); } catch (e) {}
|
||||
this.setState({ lang: l, langMenu: false }, () => this.applyLang());
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.mq) this.mq.removeEventListener("change", this.onSys);
|
||||
if (this.onResize) window.removeEventListener("resize", this.onResize);
|
||||
}
|
||||
componentDidUpdate() { this.apply(); this.applyLang(); }
|
||||
|
||||
apply() {
|
||||
const theme = this.state.theme || this.props.defaultTheme || "light";
|
||||
const dark = theme === "dark" || (theme === "system" && this.state.sysDark);
|
||||
document.documentElement.setAttribute("data-veilix-theme", dark ? "dark" : "light");
|
||||
}
|
||||
|
||||
setTheme(t) {
|
||||
try { localStorage.setItem("veilix.theme", t); } catch (e) {}
|
||||
this.setState({ theme: t }, () => this.apply());
|
||||
}
|
||||
|
||||
faq() {
|
||||
return [
|
||||
[
|
||||
["Что делать сразу после оплаты?", "Зайдите в кабинет по почте, откройте свой ключ и скачайте конфигурацию или отсканируйте QR-код с телефона. Маршрутизация уже настроена, вручную ничего вписывать не нужно."],
|
||||
["Сколько устройств можно подключить?", "До пяти на один ключ, все с одним и тем же адресом. Если нужно больше устройств или вторая страна — берёте дополнительный тариф с отдельным индивидуальным IP."],
|
||||
["Перестал работать один способ подключения.", "На вашем сервере всегда настроены оба типа: WireGuard и Xray. Откройте кабинет и подключитесь по второй конфигурации, ничего запрашивать не нужно. Набор протоколов мы дополняем и обновляем."],
|
||||
["Нужно ли что-то настраивать вручную?", "Нет. В конфигурации уже прописано, что российские сайты идут напрямую, а зарубежные — через ваш сервер. Переключать режимы во время работы не нужно."]
|
||||
],
|
||||
[
|
||||
["Почему первый месяц дороже?", "В 4 900 ₽ входит подготовка сервера: выделение адреса, настройка обоих протоколов и маршрутизации. Дальше вы платите только абонентскую плату 2 499 ₽ в месяц."],
|
||||
["Как продлевается ключ?", "Списание раз в месяц с привязанной карты. Дату окончания и карту видно на странице ключа в кабинете, там же оплату можно поменять или остановить."],
|
||||
["Можно вернуть деньги?", "Если сервер не заработал по нашей вине, возвращаем полностью в течение первых семи дней. Если вы просто передумали — возвращаем остаток неиспользованного месяца."],
|
||||
["Чем можно платить?", "Карты российских и зарубежных банков, а также криптовалюта. Все способы доступны на странице оформления заказа."]
|
||||
],
|
||||
[
|
||||
["Чем индивидуальный IP отличается от обычного VPN?", "В массовом VPN один адрес делят сотни людей, и ограничения, которые получил любой из них, касаются всех. Ваш адрес принадлежит только вам, поэтому сервисы относятся к нему как к домашнему интернету."],
|
||||
["Почему часть локаций готова только через 24 часа?", "Мы не держим пул общих адресов. Если в нужной стране свободного сервера нет, мы устанавливаем его лично под вас. Оплата первого месяца начинается с момента выдачи доступа."],
|
||||
["Зачем к тарифу российский адрес?", "Он работает в обе стороны. Из России — чтобы банки, госуслуги и стриминг видели обычное местное подключение. Из другой страны — чтобы те же сервисы открывались так, будто вы дома."],
|
||||
["Можно сменить страну позже?", "Ключ привязан к своему серверу, поэтому страну у него не меняем. Если нужна другая локация, возьмите второй ключ: он появится в том же кабинете и будет продлеваться отдельно. Первый можно оставить или отключить в любой момент."]
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
renderVals() {
|
||||
const theme = this.state.theme || this.props.defaultTheme || "light";
|
||||
const sel = "var(--accent-soft)";
|
||||
const list = this.faq()[this.state.tab];
|
||||
const v = {
|
||||
items: list.map((row, i) => ({
|
||||
q: row[0],
|
||||
a: row[1],
|
||||
off: this.state.open !== i,
|
||||
rot: this.state.open === i ? "rotate(-135deg) translateY(2px)" : "rotate(45deg) translateY(-2px)",
|
||||
toggle: () => this.setState(s => ({ open: s.open === i ? -1 : i }))
|
||||
})),
|
||||
send: () => this.setState({ sent: true }),
|
||||
navOff: this.state.narrow,
|
||||
burgerOff: !this.state.narrow,
|
||||
menuOff: !(this.state.narrow && this.state.menu),
|
||||
toggleMenu: () => this.setState(st => ({ menu: !st.menu })),
|
||||
closeMenu: () => this.setState({ menu: false }),
|
||||
toggleLangMenu: () => this.setState(st => ({ langMenu: !st.langMenu })),
|
||||
langMenuOff: !this.state.langMenu,
|
||||
langLabel: this.state.lang === "en" ? "EN" : "RU",
|
||||
ruCheck: this.state.lang === "en" ? "" : "✓",
|
||||
enCheck: this.state.lang === "en" ? "✓" : "",
|
||||
setRu: () => this.setLang("ru"),
|
||||
setEn: () => this.setLang("en"),
|
||||
ruBg: this.state.lang === "en" ? "transparent" : sel,
|
||||
enBg: this.state.lang === "en" ? sel : "transparent",
|
||||
ruInk: this.state.lang === "en" ? "var(--ink-3)" : "var(--ink)",
|
||||
enInk: this.state.lang === "en" ? "var(--ink)" : "var(--ink-3)",
|
||||
sendLabel: this.state.sent ? "Отправлено, ответим на почту" : "Отправить",
|
||||
setLight: () => this.setTheme("light"),
|
||||
setDark: () => this.setTheme("dark"),
|
||||
setSystem: () => this.setTheme("system"),
|
||||
lightBg: theme === "light" ? sel : "transparent",
|
||||
darkBg: theme === "dark" ? sel : "transparent",
|
||||
systemBg: theme === "system" ? sel : "transparent",
|
||||
lightInk: theme === "light" ? "var(--ink)" : "var(--ink-3)",
|
||||
darkInk: theme === "dark" ? "var(--ink)" : "var(--ink-3)",
|
||||
systemInk: theme === "system" ? "var(--ink)" : "var(--ink-3)"
|
||||
};
|
||||
for (let i = 0; i < 3; i++) {
|
||||
v["tab" + i] = () => this.setState({ tab: i, open: 0 });
|
||||
v["tab" + i + "Bg"] = this.state.tab === i ? sel : "transparent";
|
||||
v["tab" + i + "Border"] = this.state.tab === i ? "var(--accent)" : "var(--border)";
|
||||
v["tab" + i + "Ink"] = this.state.tab === i ? "var(--ink)" : "var(--ink-2)";
|
||||
}
|
||||
return v;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,8 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" xmlns:c2pa="http://c2pa.org/manifest"><metadata><c2pa:manifest>AAAWgmp1bWIAAAAeanVtZGMycGEAEQAQgAAAqgA4m3EDYzJwYQAAABZcanVtYgAAAEdqdW1kYzJtYQARABCAAACqADibcQN1cm46YzJwYTpkYjhkMjAzOC0wZTdmLTQ5ZDEtYmIyZi02MjU4ZDI5YjFjOGQAAAADl2p1bWIAAAApanVtZGMyYXMAEQAQgAAAqgA4m3EDYzJwYS5hc3NlcnRpb25zAAAAALxqdW1iAAAARGp1bWRjYm9yABEAEIAAAKoAOJtxE2MycGEuaW5ncmVkaWVudC52MwAAAAAYYzJzaM919rkJOu+r56vDmbDSuogAAABwY2JvcqNpZGM6Zm9ybWF0bWltYWdlL3N2Zyt4bWxqaW5zdGFuY2VJRHgseG1wOmlpZDphNDg3ZjVlYS0yNGMwLTRmMWMtYjU3Mi1jZTk0ZmNhOWRlMzVscmVsYXRpb25zaGlwaHBhcmVudE9mAAAB4mp1bWIAAABBanVtZGNib3IAEQAQgAAAqgA4m3ETYzJwYS5hY3Rpb25zLnYyAAAAABhjMnNou3TLKnEXX9d3aOK364c3gAAAAZljYm9yomdhY3Rpb25zgqJmYWN0aW9ua2MycGEub3BlbmVkanBhcmFtZXRlcnOha2luZ3JlZGllbnRzgaJjdXJseC1zZWxmI2p1bWJmPWMycGEuYXNzZXJ0aW9ucy9jMnBhLmluZ3JlZGllbnQudjNkaGFzaFggh2uOqYcsUCfLLJlcsUZW5XDb53hqcmG3yNeaOfhXo82kZmFjdGlvbngdY29tLmFudGhyb3BpYy5jbGF1ZGUucHJvdmlkZWRqcGFyYW1ldGVyc6F4H2NvbS5hbnRocm9waWMub3JpZ2luLWNvbmZpZGVuY2VndW5rbm93bmtkZXNjcmlwdGlvbnhmQ2xhdWRlIHByb3ZpZGVkIHRoaXMgZmlsZSBhdCB0aGUgcmVxdWVzdCBvZiBhIHVzZXIgYW5kIG1heSBoYXZlIGNyZWF0ZWQgb3IgbW9kaWZpZWQgdGhlIGZpbGUgY29udGVudHMubXNvZnR3YXJlQWdlbnShZG5hbWVmQ2xhdWRlcmFsbEFjdGlvbnNJbmNsdWRlZPUAAADIanVtYgAAAEBqdW1kY2JvcgARABCAAACqADibcRNjMnBhLmhhc2guZGF0YQAAAAAYYzJzaMSFmG4KKe0hfDzBQkq0FWAAAACAY2JvcqVjYWxnZnNoYTI1NmNwYWRNAAAAAAAAAAAAAAAAAGRoYXNoWCClLIpUxJ//UabRp4m2GHMZs3IWyl6ZrhiOD7Ks5Zus2GRuYW1lbmp1bWJmIG1hbmlmZXN0amV4Y2x1c2lvbnOBomVzdGFydBh7Zmxlbmd0aBkeBAAAAj5qdW1iAAAAJ2p1bWRjMmNsABEAEIAAAKoAOJtxA2MycGEuY2xhaW0udjIAAAACD2Nib3KlY2FsZ2ZzaGEyNTZpc2lnbmF0dXJleE1zZWxmI2p1bWJmPS9jMnBhL3VybjpjMnBhOmRiOGQyMDM4LTBlN2YtNDlkMS1iYjJmLTYyNThkMjliMWM4ZC9jMnBhLnNpZ25hdHVyZWppbnN0YW5jZUlEeCx4bXA6aWlkOjI1OTJhMmM5LTM5ZjgtNDA2My05ZGVjLTc3MTYzNWRkYzM0ZXJjcmVhdGVkX2Fzc2VydGlvbnODomN1cmx4LXNlbGYjanVtYmY9YzJwYS5hc3NlcnRpb25zL2MycGEuaW5ncmVkaWVudC52M2RoYXNoWCCHa46phyxQJ8ssmVyxRlblcNvneGpyYbfI15o5+FejzaJjdXJseCpzZWxmI2p1bWJmPWMycGEuYXNzZXJ0aW9ucy9jMnBhLmFjdGlvbnMudjJkaGFzaFggMi0M7Zd5/U7IYIcq59ZD8cEknO87z1xU61ChlcYW6FaiY3VybHgpc2VsZiNqdW1iZj1jMnBhLmFzc2VydGlvbnMvYzJwYS5oYXNoLmRhdGFkaGFzaFggsul0+GJ+xq2xeDU2smw5zoclumj5sx2mCDioTMkW60Z0Y2xhaW1fZ2VuZXJhdG9yX2luZm+jZG5hbWVvQW50aHJvcGljIEZpbGVzZ3ZlcnNpb25lMS4wLjBrc3BlY1ZlcnNpb25lMi40LjAAABA4anVtYgAAAChqdW1kYzJjcwARABCAAACqADibcQNjMnBhLnNpZ25hdHVyZQAAABAIY2JvctKEWQISogEmGCFZAgowggIGMIIBjaADAgECAhRA5aAK7sI50L64g/oGQgU9Z1UTADAKBggqhkjOPQQDAzBJMRcwFQYDVQQKEw5BbnRocm9waWMsIFBCQzEuMCwGA1UEAxMlQW50aHJvcGljIENvbnRlbnQgQ3JlZGVudGlhbHMgUm9vdCBDQTAeFw0yNjA4MDcxODQzNTZaFw0yODA4MDYxOTQzNTZaMEQxFzAVBgNVBAoTDkFudGhyb3BpYywgUEJDMSkwJwYDVQQDEyBBbnRocm9waWMgQ2xhdWRlIENvbnRlbnQgU2lnbmluZzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABJh6CmvLUBgFFNU0vUKlOVtE6djd17L5SuwX0LemFisBM3dkd/3cyjxFA3Qo5S46fX0/ihY0VZ7mfb9KF703t5OjWDBWMA4GA1UdDwEB/wQEAwIHgDAVBgNVHSUEDjAMBgorBgEEAYPoXgIBMAwGA1UdEwEB/wQCMAAwHwYDVR0jBBgwFoAUzlHiBIFOZFsj+OPEz5o+nMHXXMIwCgYIKoZIzj0EAwMDZwAwZAIwMXMdFJ4BetLLVY7ORuE9noqbbAZOZn/aArXyTwFAZfKrPzxF2vPoJNf1+UCdg1XGAjBwX1zd9WGqYkqmL5SFqw1QySjr1zJfpJM9+1rdDwSPLMOPOjKuiXjoU/pUUeG9RwmhY3BhZFkNngAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALine truncated
|
||||
<defs><linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop offset="0" stop-color="#00705e"></stop><stop offset="1" stop-color="#419883"></stop>
|
||||
</linearGradient></defs>
|
||||
<rect x="1" y="1" width="24" height="24" rx="8" fill="url(#g)"></rect>
|
||||
<rect x="7" y="7" width="24" height="24" rx="8" fill="#a9dfd2"></rect>
|
||||
<rect x="7.7" y="7.7" width="22.6" height="22.6" rx="7.3" fill="none" stroke="#00705e" stroke-opacity="0.55" stroke-width="1.4"></rect>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 8.1 KiB |
@@ -0,0 +1,8 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" xmlns:c2pa="http://c2pa.org/manifest"><metadata><c2pa:manifest>AAAWgmp1bWIAAAAeanVtZGMycGEAEQAQgAAAqgA4m3EDYzJwYQAAABZcanVtYgAAAEdqdW1kYzJtYQARABCAAACqADibcQN1cm46YzJwYToyYmQ5ZmU0Yy1lYTYwLTQyZTgtYTYwMC1kM2YxN2ZlMWZmNWQAAAADl2p1bWIAAAApanVtZGMyYXMAEQAQgAAAqgA4m3EDYzJwYS5hc3NlcnRpb25zAAAAALxqdW1iAAAARGp1bWRjYm9yABEAEIAAAKoAOJtxE2MycGEuaW5ncmVkaWVudC52MwAAAAAYYzJzaP7mE7KwatMQ9m/EB5bQf0sAAABwY2JvcqNpZGM6Zm9ybWF0bWltYWdlL3N2Zyt4bWxqaW5zdGFuY2VJRHgseG1wOmlpZDoxODIyYmQwYy01MDA2LTQxNGMtOGI3Yi0xMDlhZTA2OTQ5N2ZscmVsYXRpb25zaGlwaHBhcmVudE9mAAAB4mp1bWIAAABBanVtZGNib3IAEQAQgAAAqgA4m3ETYzJwYS5hY3Rpb25zLnYyAAAAABhjMnNoYTB+WI41NX8SM923oLhJ1wAAAZljYm9yomdhY3Rpb25zgqJmYWN0aW9ua2MycGEub3BlbmVkanBhcmFtZXRlcnOha2luZ3JlZGllbnRzgaJjdXJseC1zZWxmI2p1bWJmPWMycGEuYXNzZXJ0aW9ucy9jMnBhLmluZ3JlZGllbnQudjNkaGFzaFgg3E108VpPF0A89PCiPkJvU0xFRCjEsYXd9w6fJa/3hpSkZmFjdGlvbngdY29tLmFudGhyb3BpYy5jbGF1ZGUucHJvdmlkZWRqcGFyYW1ldGVyc6F4H2NvbS5hbnRocm9waWMub3JpZ2luLWNvbmZpZGVuY2VndW5rbm93bmtkZXNjcmlwdGlvbnhmQ2xhdWRlIHByb3ZpZGVkIHRoaXMgZmlsZSBhdCB0aGUgcmVxdWVzdCBvZiBhIHVzZXIgYW5kIG1heSBoYXZlIGNyZWF0ZWQgb3IgbW9kaWZpZWQgdGhlIGZpbGUgY29udGVudHMubXNvZnR3YXJlQWdlbnShZG5hbWVmQ2xhdWRlcmFsbEFjdGlvbnNJbmNsdWRlZPUAAADIanVtYgAAAEBqdW1kY2JvcgARABCAAACqADibcRNjMnBhLmhhc2guZGF0YQAAAAAYYzJzaP0IC7BR054ns2mFgoQ6hCcAAACAY2JvcqVjYWxnZnNoYTI1NmNwYWRNAAAAAAAAAAAAAAAAAGRoYXNoWCC92+BR8qFvAXAhWyDvg9hz/npVNqNBZTe8aAHtqKCqXmRuYW1lbmp1bWJmIG1hbmlmZXN0amV4Y2x1c2lvbnOBomVzdGFydBh7Zmxlbmd0aBkeBAAAAj5qdW1iAAAAJ2p1bWRjMmNsABEAEIAAAKoAOJtxA2MycGEuY2xhaW0udjIAAAACD2Nib3KlY2FsZ2ZzaGEyNTZpc2lnbmF0dXJleE1zZWxmI2p1bWJmPS9jMnBhL3VybjpjMnBhOjJiZDlmZTRjLWVhNjAtNDJlOC1hNjAwLWQzZjE3ZmUxZmY1ZC9jMnBhLnNpZ25hdHVyZWppbnN0YW5jZUlEeCx4bXA6aWlkOjA3MzMyY2RhLTljZWMtNGExZC1hYWJmLWU2OWQ0NTI5MDgwZHJjcmVhdGVkX2Fzc2VydGlvbnODomN1cmx4LXNlbGYjanVtYmY9YzJwYS5hc3NlcnRpb25zL2MycGEuaW5ncmVkaWVudC52M2RoYXNoWCDcTXTxWk8XQDz08KI+Qm9TTEVEKMSxhd33Dp8lr/eGlKJjdXJseCpzZWxmI2p1bWJmPWMycGEuYXNzZXJ0aW9ucy9jMnBhLmFjdGlvbnMudjJkaGFzaFggAOYiBKaqhWClbcPbmxFMjwITDHZ752RjpbEazyMlcrmiY3VybHgpc2VsZiNqdW1iZj1jMnBhLmFzc2VydGlvbnMvYzJwYS5oYXNoLmRhdGFkaGFzaFgge2AxpUBp9srweE9dMa1k1kbdgI4m5KetgQH8+8GInxB0Y2xhaW1fZ2VuZXJhdG9yX2luZm+jZG5hbWVvQW50aHJvcGljIEZpbGVzZ3ZlcnNpb25lMS4wLjBrc3BlY1ZlcnNpb25lMi40LjAAABA4anVtYgAAAChqdW1kYzJjcwARABCAAACqADibcQNjMnBhLnNpZ25hdHVyZQAAABAIY2JvctKEWQISogEmGCFZAgowggIGMIIBjaADAgECAhRA5aAK7sI50L64g/oGQgU9Z1UTADAKBggqhkjOPQQDAzBJMRcwFQYDVQQKEw5BbnRocm9waWMsIFBCQzEuMCwGA1UEAxMlQW50aHJvcGljIENvbnRlbnQgQ3JlZGVudGlhbHMgUm9vdCBDQTAeFw0yNjA4MDcxODQzNTZaFw0yODA4MDYxOTQzNTZaMEQxFzAVBgNVBAoTDkFudGhyb3BpYywgUEJDMSkwJwYDVQQDEyBBbnRocm9waWMgQ2xhdWRlIENvbnRlbnQgU2lnbmluZzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABJh6CmvLUBgFFNU0vUKlOVtE6djd17L5SuwX0LemFisBM3dkd/3cyjxFA3Qo5S46fX0/ihY0VZ7mfb9KF703t5OjWDBWMA4GA1UdDwEB/wQEAwIHgDAVBgNVHSUEDjAMBgorBgEEAYPoXgIBMAwGA1UdEwEB/wQCMAAwHwYDVR0jBBgwFoAUzlHiBIFOZFsj+OPEz5o+nMHXXMIwCgYIKoZIzj0EAwMDZwAwZAIwMXMdFJ4BetLLVY7ORuE9noqbbAZOZn/aArXyTwFAZfKrPzxF2vPoJNf1+UCdg1XGAjBwX1zd9WGqYkqmL5SFqw1QySjr1zJfpJM9+1rdDwSPLMOPOjKuiXjoU/pUUeG9RwmhY3BhZFkNngAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALine truncated
|
||||
<defs><linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop offset="0" stop-color="#6137a6"></stop><stop offset="1" stop-color="#9c66b6"></stop>
|
||||
</linearGradient></defs>
|
||||
<rect x="1" y="1" width="24" height="24" rx="8" fill="url(#g)"></rect>
|
||||
<rect x="7" y="7" width="24" height="24" rx="8" fill="#d9c4ea"></rect>
|
||||
<rect x="7.7" y="7.7" width="22.6" height="22.6" rx="7.3" fill="none" stroke="#6137a6" stroke-opacity="0.55" stroke-width="1.4"></rect>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 8.1 KiB |
@@ -0,0 +1,597 @@
|
||||
// Veilix — словарь RU → EN. Источник перевода для фронтенда.
|
||||
// Ключ — строка как она написана в разметке, значение — английский вариант.
|
||||
window.VEILIX_I18N = {
|
||||
// Навигация и общее
|
||||
"Цены": "Pricing",
|
||||
"Локации": "Locations",
|
||||
"Поддержка": "Support",
|
||||
"Войти": "Log in",
|
||||
"Получить VPN": "Get a VPN",
|
||||
"Купить": "Buy",
|
||||
"Отменить": "Cancel",
|
||||
"Шесть стран на выбор и российский адрес в комплекте с любым тарифом.": "Six countries to choose from, with a Russian address included in every plan.",
|
||||
"RU-адрес входит в каждый тариф": "A RU address comes with every plan",
|
||||
"Выберите страну": "Choose a country",
|
||||
"Российский адрес в каждом тарифе": "A Russian address in every plan",
|
||||
"Он работает в обе стороны: из России банки и госуслуги видят обычное местное подключение, из другой страны те же сервисы открываются так, будто вы дома.": "It works both ways: from Russia, banks and government services see an ordinary local connection; from abroad, those same services open as if you were at home.",
|
||||
"Нужной страны нет в списке": "Your country is not on the list",
|
||||
"Мы устанавливаем серверы и в других странах. Напишите, какая нужна, — скажем сроки и установим под вас.": "We set up servers in other countries too. Tell us which one you need and we will give you the timing and build it for you.",
|
||||
"Условия": "Terms",
|
||||
"Конфиденциальность": "Privacy",
|
||||
"Реферальная программа": "Referral program",
|
||||
"Ваша ссылка": "Your link",
|
||||
"Ожидает подтверждения": "Awaiting confirmation",
|
||||
"Месяц начисляется вам и другу после его первой оплаты и применяется к ближайшему списанию.": "The month is credited after your friend's first payment and applies to the next charge.",
|
||||
"Приглашённые": "Invitees",
|
||||
"Месяц начислен": "Month credited",
|
||||
"Ждём оплаты": "Awaiting payment",
|
||||
"Реферальная ссылка": "Referral link",
|
||||
"Приведите друга — вы оба получите по месяцу в подарок.": "Invite a friend and you both get a free month.",
|
||||
"Приглашено": "Invited",
|
||||
"Месяцев начислено": "Months earned",
|
||||
"Поделиться": "Share",
|
||||
"Ссылка скопирована": "Link copied",
|
||||
"Копировать ссылку": "Copy link",
|
||||
"Сервис": "Service",
|
||||
"Документы": "Legal",
|
||||
"Тема": "Theme",
|
||||
"Язык": "Language",
|
||||
"Условия использования": "Terms of use",
|
||||
"Политика конфиденциальности": "Privacy policy",
|
||||
"Возврат средств": "Refunds",
|
||||
"Вейликс": "Veilix",
|
||||
"Адрес принадлежит только вам: один и тот же IP на всех устройствах, в любой стране. Без соседей по серверу и их репутации.": "The address belongs to you alone: the same IP on every device, in any country. No neighbours on the server and no reputation to inherit.",
|
||||
"Российский адрес в подписке": "Russian address in the subscription",
|
||||
"Нужной страны нет в списке или нужна подписка на команду — напишите, ответим в течение дня.": "If the country you need is missing or you want a team subscription, write to us and we will reply within a day.",
|
||||
"Скоро": "Coming soon",
|
||||
"Доступно в подписке": "Included in the subscription",
|
||||
"VPN, который не нужно отключать. Работаем с 2022 года.": "A VPN you never have to switch off. Running since 2022.",
|
||||
"30 стран и российский адрес входят в подписку.": "Thirty countries and a Russian address come with the subscription.",
|
||||
"Одна цена за всё: 699 ₽ в месяц или 6 990 ₽ в год.": "One price for everything: 699 ₽ a month or 6,990 ₽ a year.",
|
||||
"Одна цена за всё. Отменить продление можно в кабинете.": "One price for everything. You can cancel renewal in your account.",
|
||||
"Все локации доступны сразу после оплаты и входят в одну цену.": "Every location is available right after payment and included in one price.",
|
||||
"Тридцать локаций и российский адрес входят в подписку. Переключайтесь между странами сколько угодно, на пяти устройствах сразу.": "Thirty locations and a Russian address come with the subscription. Switch countries as often as you like, on five devices at once.",
|
||||
"VPN, который не нужно отключать": "A VPN you never have to switch off",
|
||||
"Российские сайты напрямую": "Russian sites go direct",
|
||||
"Банки и госуслуги работают как обычно, отключать VPN не нужно.": "Banks and government services keep working as usual, with no need to switch the VPN off.",
|
||||
"Современный протокол XRAY": "Modern XRAY protocol",
|
||||
"Трафик неотличим от обычного HTTPS, поэтому подключение держится там, где старые протоколы уже не работают.": "The traffic is indistinguishable from ordinary HTTPS, so the connection holds where older protocols no longer work.",
|
||||
"На каком протоколе работает подключение?": "Which protocol does the connection use?",
|
||||
"На XRAY — это современный протокол с VLESS, Hysteria2 и XHTTP. Трафик выглядит как обычный HTTPS, поэтому соединение держится там, где старые протоколы блокируются.": "XRAY — a modern protocol with VLESS, Hysteria2 and XHTTP. The traffic looks like ordinary HTTPS, so the connection holds where older protocols get blocked.",
|
||||
"Все тридцать стран, включая российскую локацию, и пять устройств. Отдельной платы за страну или устройство нет.": "All thirty countries including the Russian location, and five devices. There is no separate charge per country or device.",
|
||||
"Конфигурации XRAY": "XRAY configurations",
|
||||
"Ссылка подключения": "Connection link",
|
||||
"Для Happ и других клиентов XRAY": "For Happ and other XRAY clients",
|
||||
"Быстрый способ подключить новое устройство. Скопируйте ссылку или отсканируйте код — настройки те же.": "A quick way to add a new device. Copy the link or scan the code — the settings are the same.",
|
||||
"На компьютере скачайте файл и откройте его в клиенте XRAY. На телефоне — отсканируйте QR-код.": "On a computer download the file and open it in an XRAY client. On a phone, scan the QR code.",
|
||||
"Откройте подключение, скачайте любую свободную конфигурацию XRAY или отсканируйте её QR-код. Каждая конфигурация рассчитана на одно устройство.": "Open the connection, download any free XRAY configuration or scan its QR code. Each configuration covers one device.",
|
||||
"Обновите конфигурацию в кабинете или смените локацию — это занимает пару секунд. Если не помогло, напишите в поддержку, ответим в течение часа.": "Refresh the configuration in your account or change location — it takes a couple of seconds. If that doesn't help, write to support and we will reply within an hour.",
|
||||
"Подключение работает на XRAY: откройте кабинет и обновите конфигурацию или смените локацию. Настройки протокола мы обновляем по мере блокировок.": "The connection runs on XRAY: open your account and refresh the configuration or change location. We update the protocol settings as blocks appear.",
|
||||
"Зайдите в кабинет по почте, выберите страну и скачайте конфигурацию XRAY или отсканируйте QR-код с телефона. Маршрутизация уже настроена, вручную ничего вписывать не нужно.": "Sign in with your email, choose a country and download the XRAY configuration or scan the QR code with your phone. Routing is already set up, nothing to enter by hand.",
|
||||
"Наведите камеру телефона на код в приложении Happ или другом клиенте XRAY.": "Point your phone camera at the code in Happ or another XRAY client.",
|
||||
"Откройте подключение, скачайте любую свободную конфигурацию WireGuard или отсканируйте её QR-код. Каждая конфигурация рассчитана на одно устройство.": "Open the connection, download any free WireGuard configuration or scan its QR code. Each configuration covers one device.",
|
||||
"Переключитесь на Xray в той же локации — он настроен заранее. Если не помогло, напишите в поддержку, ответим в течение часа.": "Switch to Xray in the same location — it is set up in advance. If that doesn't help, write to support and we will reply within an hour.",
|
||||
"В разделе оплаты нажмите «Изменить карту» и отвяжите её. Подписка доработает оплаченный период.": "In the payment section tap “Change card” and unlink it. The subscription runs to the end of the paid period.",
|
||||
"VPN со всеми странами в одной подписке. Работаем с 2022 года.": "A VPN with every country in one subscription. Running since 2022.",
|
||||
"или 6 990 ₽ в год — 582 ₽ в месяц": "or 6,990 ₽ a year — 582 ₽ per month",
|
||||
"Одна цена за всё: страны не тарифицируются отдельно, платы за подключение нет. Отменить продление можно в кабинете.": "One price for everything: no per-country charges and no setup fee. You can cancel renewal in your account.",
|
||||
"Все 30 стран, переключение в любой момент": "All 30 countries, switch any time",
|
||||
"Российская локация: прямое соединение и мост в обе стороны": "Russian location: direct connection and a bridge both ways",
|
||||
"Все локации доступны сразу после оплаты и входят в одну цену. Переключаться между ними можно сколько угодно раз.": "Every location is available right after payment and included in one price. Switch between them as often as you like.",
|
||||
"Включено": "Included",
|
||||
"30 стран и российский адрес входят в подписку. Переключайтесь между ними сколько угодно раз.": "Thirty countries and a Russian address come with the subscription. Switch between them as often as you like.",
|
||||
"Нажмите точку на карте — покажем страну и город": "Tap a dot on the map to see the country and city",
|
||||
"Доступно сразу": "Available now",
|
||||
"RU-адрес входит в подписку": "Russian address included",
|
||||
"Все страны подписки": "Every country in the subscription",
|
||||
"Одна цена за всё: 699 ₽ в месяц или 6 990 ₽ в год. Страны не тарифицируются отдельно.": "One price for everything: 699 ₽ a month or 6,990 ₽ a year. Countries are not charged separately.",
|
||||
"Локации и подписка": "Locations and subscription",
|
||||
"Тема: локации и подписка": "Topic: locations and subscription",
|
||||
"Если подписка уже оформлена, укажите почту, на которую она оформлена — так мы сразу увидим вашу конфигурацию.": "If you already have a subscription, give the email it is registered to and we will see your configuration right away.",
|
||||
"Нужны особые условия": "Need special terms",
|
||||
"Подписки на команду, оплата по счёту или редкая страна. Оставьте заявку — ответим в течение дня.": "Team subscriptions, invoice payment or a rare country. Leave a request and we will reply within a day.",
|
||||
"Подписка ВейликсВПН": "VeilixVPN subscription",
|
||||
"30 стран и российский адрес": "30 countries and a Russian address",
|
||||
"Доступ открывается сразу после оплаты": "Access opens right after payment",
|
||||
"Месяц · 699 ₽": "Monthly · 699 ₽",
|
||||
"Год · 6 990 ₽": "Yearly · 6,990 ₽",
|
||||
"все 30": "all 30",
|
||||
"Ваша подписка": "Your subscription",
|
||||
"Тридцать стран и российский адрес, до пяти устройств.": "Thirty countries and a Russian address, up to five devices.",
|
||||
"Хотите другую страну": "Want a different country",
|
||||
"Все локации уже в подписке, переключение бесплатное.": "Every location is already in the subscription and switching is free.",
|
||||
"Сменить страну": "Change country",
|
||||
"Подключения": "Connections",
|
||||
"← Все подключения": "← All connections",
|
||||
"699 ₽ в месяц": "699 ₽ per month",
|
||||
"Каждое в своей стране": "Each in its own country",
|
||||
"Выбрать страну": "Choose a country",
|
||||
"Все тридцать локаций входят в подписку. Переключение бесплатное и занимает пару секунд.": "All thirty locations come with the subscription. Switching is free and takes a couple of seconds.",
|
||||
"Выбрать": "Choose",
|
||||
"Все страны входят в подписку, переключение бесплатное": "Every country is included, switching is free",
|
||||
"Переключиться": "Switch",
|
||||
"Смена локации применяется сразу, платить ничего не нужно.": "The new location applies immediately and costs nothing.",
|
||||
"Подписка": "Subscription",
|
||||
"Страны": "Countries",
|
||||
"Активна": "Active",
|
||||
"Готова": "Ready",
|
||||
"Оплачено. Подписка уже в кабинете — ": "Paid. Your subscription is in the account — ",
|
||||
"Сколько стоит подписка?": "How much does the subscription cost?",
|
||||
"699 ₽ в месяц или 6 990 ₽ в год. Годовая выходит по 582 ₽ в месяц и экономит 1 398 ₽. Платы за подключение и доплат за страну нет.": "699 ₽ a month or 6,990 ₽ a year. The yearly plan works out at 582 ₽ a month and saves 1,398 ₽. There is no setup fee and no per-country charge.",
|
||||
"Как продлевается подписка?": "How is the subscription renewed?",
|
||||
"Списание раз в месяц или раз в год с привязанной карты. Дату окончания и карту видно в кабинете, там же оплату можно поменять или остановить.": "Charged monthly or yearly to the linked card. The end date and the card are shown in your account, where you can change or stop payment.",
|
||||
"Сколько стран входит в подписку?": "How many countries are included?",
|
||||
"Тридцать, включая российскую локацию. Все они доступны сразу и не тарифицируются отдельно.": "Thirty, including the Russian location. All of them are available immediately and are not charged separately.",
|
||||
"Чем это отличается от индивидуального IP?": "How is this different from a dedicated IP?",
|
||||
"Здесь адрес общий: на сервере несколько человек, поэтому подписка стоит заметно дешевле. Если нужен адрес, закреплённый только за вами, это отдельные тарифы Вейликс с индивидуальным IP.": "The address here is shared: several people use one server, so the subscription costs much less. If you need an address reserved for you alone, Veilix has separate dedicated-IP plans.",
|
||||
"Зачем в подписке российский адрес?": "Why does the subscription include a Russian address?",
|
||||
"До пяти на одну подписку. Страны у устройств могут быть разные: каждое подключается к своей локации.": "Up to five per subscription. Devices can use different countries: each connects to its own location.",
|
||||
"Локация доступна сразу, переключение бесплатное": "The location is available now and switching is free",
|
||||
"ВейликсВПН": "VeilixVPN",
|
||||
"© 2026 ВейликсВПН": "© 2026 VeilixVPN",
|
||||
"Все страны в одной подписке": "Every country in one subscription",
|
||||
"Быстрый VPN со всеми странами сразу": "A fast VPN with every country included",
|
||||
"Тридцать локаций и российский адрес входят в подписку. Переключайтесь между странами сколько угодно, на пяти устройствах сразу, без лимитов трафика и скорости.": "Thirty locations and a Russian address come with the subscription. Switch countries as often as you like, on five devices at once, with no traffic or speed limits.",
|
||||
"699 ₽ в месяц или 6 990 ₽ в год": "699 ₽ a month or 6,990 ₽ a year",
|
||||
"RU-адрес включён": "Russian address included",
|
||||
"30 стран, переключение в один тап": "30 countries, one tap to switch",
|
||||
"Все локации в подписке": "Every location in the subscription",
|
||||
"Список стран открыт целиком. Выбираете страну в приложении, соединение перестраивается за пару секунд, доплачивать за переключение не нужно.": "The whole country list is open. Pick a country in the app, the connection rebuilds in a couple of seconds, and switching costs nothing extra.",
|
||||
"Тридцать стран": "Thirty countries",
|
||||
"Европа, США, Азия, Ближний Восток и Южная Америка. Список пополняется.": "Europe, the US, Asia, the Middle East and South America. The list keeps growing.",
|
||||
"Пять устройств сразу": "Five devices at once",
|
||||
"Телефон, ноутбук, планшет и ещё два устройства работают одновременно.": "Phone, laptop, tablet and two more devices all run at the same time.",
|
||||
"Без лимитов трафика": "No traffic limits",
|
||||
"Скорость не режем, объём не считаем, ограничений по времени нет.": "We don't throttle speed, count traffic or limit time.",
|
||||
"Пять устройств в подписке": "Five devices per subscription",
|
||||
"Одна подписка на всю технику. Устройства работают одновременно и в разных странах.": "One subscription for all your devices. They run at the same time and in different countries.",
|
||||
"Российский адрес включён": "Russian address included",
|
||||
"Российская локация входит в подписку: банки, госуслуги и стриминг открываются как дома.": "The Russian location is part of the subscription: banks, government services and streaming open as if you were home.",
|
||||
"туннелем в выбранную локацию": "tunnelled to the chosen location",
|
||||
"прямо из выбранной локации": "straight from the chosen location",
|
||||
"Одна подписка, все страны": "One subscription, every country",
|
||||
"Тридцать локаций, российский адрес и пять устройств.": "Thirty locations, a Russian address and five devices.",
|
||||
"Все 30 стран и российский адрес": "All 30 countries and a Russian address",
|
||||
"До 5 устройств одновременно": "Up to 5 devices at once",
|
||||
"Без лимитов трафика и скорости": "No traffic or speed limits",
|
||||
"Оба типа подключения на выбор": "Both connection types available",
|
||||
"Страны в подписке": "Countries included",
|
||||
"Месяц": "Monthly",
|
||||
"699 ₽": "699 ₽",
|
||||
"Списание раз в месяц, отмена в любой момент": "Charged monthly, cancel any time",
|
||||
"Год": "Yearly",
|
||||
"выгоднее": "better value",
|
||||
"6 990 ₽": "6,990 ₽",
|
||||
"582 ₽ в месяц, экономия 1 398 ₽ за год": "582 ₽ a month, saving 1,398 ₽ a year",
|
||||
"Оформить подписку": "Get the subscription",
|
||||
"Карты российских и зарубежных банков, криптовалюта. Доступ открывается сразу после оплаты.": "Russian and international cards, plus crypto. Access opens right after payment.",
|
||||
"Нужен адрес, закреплённый только за вами": "Need an address that is yours alone",
|
||||
"Здесь сервер общий: он дешевле и этого достаточно большинству. Если сервисы требуют постоянный IP, посмотрите индивидуальные тарифы Вейликс.": "The server here is shared: it costs less and is enough for most people. If a service needs a permanent IP, look at Veilix dedicated plans.",
|
||||
"Что входит в подписку?": "What is included in the subscription?",
|
||||
"Все тридцать стран, включая российскую локацию, пять устройств и оба типа подключения. Отдельной платы за страну или устройство нет.": "All thirty countries including the Russian location, five devices and both connection types. There is no separate charge per country or device.",
|
||||
"Можно менять страну сколько угодно?": "Can I change the country as often as I want?",
|
||||
"Да. Переключение занимает пару секунд и ничего не стоит: список локаций открыт целиком на всё время подписки.": "Yes. Switching takes a couple of seconds and costs nothing: the full location list stays open for the whole subscription.",
|
||||
"Нет. В кабинете вы скачиваете готовый файл или сканируете QR-код с телефона. Российские сайты идут напрямую, зарубежные — через выбранную локацию.": "No. In your account you download a ready file or scan a QR code with your phone. Russian sites go direct, the rest travel through the chosen location.",
|
||||
"Есть ограничения по скорости и трафику?": "Are there speed or traffic limits?",
|
||||
"Нет. Объём не считаем и скорость не режем: каналов хватает на видео в высоком качестве и звонки.": "No. We don't count traffic or throttle speed: the channels handle high-quality video and calls.",
|
||||
"Чем это отличается от индивидуального тарифа?": "How is this different from a dedicated plan?",
|
||||
"Здесь адрес общий: на сервере несколько человек, поэтому подписка стоит заметно дешевле. Если нужен адрес, закреплённый только за вами, это отдельные тарифы с индивидуальным IP.": "The address here is shared: several people use one server, which makes the subscription much cheaper. If you need an address reserved for you alone, that is the dedicated IP plan.",
|
||||
"Как оплачивается и продлевается?": "How is it paid and renewed?",
|
||||
"699 ₽ в месяц или 6 990 ₽ в год со списанием с привязанной карты. Годовая подписка дешевле помесячной на 1 398 ₽. Отменить продление можно в кабинете в любой момент.": "699 ₽ a month or 6,990 ₽ a year, charged to the linked card. The yearly plan is 1,398 ₽ cheaper than paying monthly. You can cancel renewal in your account at any time.",
|
||||
"© 2026 Вейликс": "© 2026 Veilix",
|
||||
"Персональные VPN-серверы с индивидуальным IP. Работаем с 2022 года.": "Personal VPN servers with a dedicated IP. Running since 2022.",
|
||||
"Русский": "Russian",
|
||||
"English": "English",
|
||||
"скоро": "soon",
|
||||
|
||||
// Главная — hero
|
||||
"Персональный сервер, не общий": "A personal server, not a shared one",
|
||||
"Работаем с 2022 года": "Running since 2022",
|
||||
"Ваш личный VPN с индивидуальным IP": "Your private VPN with a dedicated IP",
|
||||
"Адрес принадлежит только вам: один и тот же IP на всех устройствах, в любой стране. Без соседей по серверу, без общих подсетей и их репутации.": "The address belongs to you alone: the same IP on every device, in any country. No neighbours on the server, no shared subnets and their reputation.",
|
||||
"Посмотреть локации": "See locations",
|
||||
"4 900 ₽ первый месяц, далее 2 499 ₽": "$49 first month, then $24",
|
||||
"4 900 ₽": "$49",
|
||||
"2 900 ₽": "$24",
|
||||
"2 499 ₽": "$24",
|
||||
"490 ₽": "$5",
|
||||
"До 5 устройств": "Up to 5 devices",
|
||||
"RU-адрес в комплекте": "RU address included",
|
||||
"Ваш IP закреплён только за вами": "Your IP is reserved for you alone",
|
||||
|
||||
// Главная — преимущества
|
||||
"Что это даёт на практике": "What this means in practice",
|
||||
"Индивидуальный IP": "Dedicated IP",
|
||||
"Адрес выделен под вас одного. Сервисы видят обычного человека из выбранной страны, а не сотню людей за одним адресом.": "The address is assigned to you alone. Services see one ordinary person from the country you chose, not a hundred people behind a single address.",
|
||||
"Западные AI-сервисы": "Western AI services",
|
||||
"Риск блокировки ниже, чем у публичных VPN: их адреса уже давно в чёрных списках.": "Lower risk of being blocked than with public VPNs, whose addresses have long been blacklisted.",
|
||||
"Один адрес на всех устройствах": "One address across all devices",
|
||||
"Ноутбук, телефон, планшет выглядят одинаково. Вы всегда в одной локации со своим IP.": "Laptop, phone and tablet all look the same. You always stay in one location with your own IP.",
|
||||
"Меньше проверок и капч": "Fewer checks and captchas",
|
||||
"На массовых адресах срабатывает антибот-защита. На личном сервисы работают стабильнее.": "Mass addresses trigger anti-bot protection. On a personal one, services behave more predictably.",
|
||||
"Трафик под шифрованием": "Encrypted traffic",
|
||||
"Дома, в отеле или в кафе соединение защищено в любой точке мира.": "At home, in a hotel or in a café, the connection is protected anywhere in the world.",
|
||||
"До 5 устройств в тарифе": "Up to 5 devices per plan",
|
||||
"Нужно больше или другая страна — добавляете второй тариф со своим отдельным IP.": "Need more, or another country — add a second plan with its own separate IP.",
|
||||
"Плюс выделенный RU-адрес": "Plus a dedicated RU address",
|
||||
"Идёт к любому тарифу. Работает и как прямое соединение, и как мост в обе стороны.": "Comes with every plan. Works both as a direct connection and as a two-way bridge.",
|
||||
"Два типа подключения": "Two connection types",
|
||||
"Если один способ перестал работать, второй уже настроен. Список дополняется и обновляется.": "If one method stops working, the second is already set up. The list is extended and updated over time.",
|
||||
"Трафик идёт коротким путём": "Traffic takes the short route",
|
||||
"Маршрут выбирается сам, по адресу сайта. Российские сервисы не ломаются, зарубежные открываются, переключать ничего не нужно.": "The route is chosen automatically by the site address. Russian services keep working, foreign ones open, and you never switch anything.",
|
||||
"Вы в России": "You are in Russia",
|
||||
"Вы за границей": "You are abroad",
|
||||
"RU-сайты": "RU sites",
|
||||
"Остальной мир": "The rest of the world",
|
||||
"Мировые сайты": "Global sites",
|
||||
"напрямую через RU-сервер": "directly through the RU server",
|
||||
"туннелем в вашу локацию": "tunnelled to your location",
|
||||
"прямо из вашей локации": "straight from your location",
|
||||
"туннелем через RU-сервер": "tunnelled through the RU server",
|
||||
"Для тех, кто работает с Россией из другой страны.": "For people who work with Russia from another country.",
|
||||
|
||||
// Главная — цены и вопросы
|
||||
"Один тариф, без уровней": "One plan, no tiers",
|
||||
"первый месяц": "first month",
|
||||
"далее 2 499 ₽ в месяц": "then $24 per month",
|
||||
"Индивидуальный IP в выбранной стране": "Dedicated IP in the country you choose",
|
||||
"Выделенный RU-адрес в комплекте": "Dedicated RU address included",
|
||||
"До 5 устройств": "Up to 5 devices",
|
||||
"2 типа подключения:": "2 connection types:",
|
||||
"WireGuard / Amnezia WG": "WireGuard / Amnezia WG",
|
||||
"Xray: VLESS, Hysteria2, XHTTP": "Xray: VLESS, Hysteria2, XHTTP",
|
||||
"Выберите локацию": "Choose a location",
|
||||
"Свободные серверы есть сейчас": "Servers are available right now",
|
||||
"Сервер установим под вас за 24 часа": "We will set up a server for you within 24 hours",
|
||||
"Нидерланды · Амстердам": "Netherlands · Amsterdam",
|
||||
"Германия · Франкфурт": "Germany · Frankfurt",
|
||||
"США · Нью-Йорк": "United States · New York",
|
||||
"Финляндия · Хельсинки": "Finland · Helsinki",
|
||||
"Турция · Стамбул": "Türkiye · Istanbul",
|
||||
"Япония · Токио": "Japan · Tokyo",
|
||||
"Нидерланды": "Netherlands",
|
||||
"Германия": "Germany",
|
||||
"США": "United States",
|
||||
"Финляндия": "Finland",
|
||||
"Турция": "Türkiye",
|
||||
"Япония": "Japan",
|
||||
"Амстердам": "Amsterdam",
|
||||
"Франкфурт": "Frankfurt",
|
||||
"Нью-Йорк": "New York",
|
||||
"Хельсинки": "Helsinki",
|
||||
"Стамбул": "Istanbul",
|
||||
"Токио": "Tokyo",
|
||||
"3 сервера в наличии": "3 servers available",
|
||||
"2 сервера в наличии": "2 servers available",
|
||||
"1 сервер в наличии": "1 server available",
|
||||
"готово за 24 часа": "ready within 24 hours",
|
||||
"Установим за 24 часа": "Ready within 24 hours",
|
||||
"Серверы в наличии": "Servers available",
|
||||
"Нужны индивидуальные условия": "Need custom terms",
|
||||
"Вопросы": "Questions",
|
||||
"Чем это отличается от обычного VPN за 300 рублей?": "How is this different from an ordinary 300-rouble VPN?",
|
||||
"В массовом VPN ваш адрес делят сотни людей. Если кто-то из них попал в чёрный список, ограничения касаются всех. У вас адрес один и только ваш, поэтому сервисы относятся к нему как к домашнему интернету.": "In a mass-market VPN hundreds of people share your address. If any of them ends up blacklisted, the restrictions apply to everyone. Here the address is yours alone, so services treat it like a home connection.",
|
||||
"Почему некоторые локации готовы только через 24 часа?": "Why are some locations only ready in 24 hours?",
|
||||
"Мы не держим пул общих адресов. Если в нужной стране свободных серверов нет, мы устанавливаем сервер лично под вас и выдаём доступ в течение суток. Оплата за первый месяц начинается с момента выдачи.": "We do not keep a pool of shared addresses. If there is no free server in the country you need, we set one up for you personally and hand over access within a day. The first month starts when access is issued.",
|
||||
"Нужно ли что-то настраивать самому?": "Do I have to configure anything myself?",
|
||||
"Нет. В личном кабинете вы скачиваете готовый файл или сканируете QR-код с телефона. Маршрутизация уже настроена: российские сайты идут напрямую, зарубежные — через ваш сервер.": "No. In your account you download a ready-made file or scan a QR code with your phone. Routing is already set up: Russian sites go direct, foreign ones through your server.",
|
||||
"Сколько устройств можно подключить?": "How many devices can I connect?",
|
||||
"До пяти на один тариф, все с одним и тем же адресом. Нужно больше устройств или вторая страна — берёте дополнительный тариф с отдельным индивидуальным IP.": "Up to five per plan, all sharing the same address. If you need more devices or a second country, take an additional plan with its own dedicated IP.",
|
||||
"Зачем мне ещё и российский адрес?": "Why do I also need a Russian address?",
|
||||
"Он работает в обе стороны. Из России — чтобы банки, госуслуги и стриминг видели обычное местное подключение. Из другой страны — чтобы те же сервисы открывались так, будто вы дома.": "It works both ways. From Russia, so that banks, government services and streaming see an ordinary local connection. From abroad, so the same services open as if you were at home.",
|
||||
"Что будет, если способ подключения перестанет работать?": "What if a connection method stops working?",
|
||||
"На вашем сервере одновременно работают два типа подключения. Если один способ начал блокироваться, вы переключаетесь на второй в том же кабинете. Набор протоколов мы дополняем и обновляем.": "Your server runs both connection types at once. If one starts getting blocked, you switch to the other in the same account. We keep extending and updating the set of protocols.",
|
||||
|
||||
// Цены
|
||||
"Одна цена, без уровней и апсейлов": "One price, no tiers and no upsells",
|
||||
"В первый месяц входит подготовка сервера под вас. Дальше — только абонентская плата, отменить можно в кабинете.": "The first month covers preparing the server for you. After that it is only the subscription fee, and you can cancel in your account.",
|
||||
"Что входит": "What is included",
|
||||
"Индивидуальный IP в выбранной стране, только ваш": "A dedicated IP in the country you choose, yours alone",
|
||||
"Выделенный RU-адрес: прямое соединение и мост в обе стороны": "A dedicated RU address: direct connection and a two-way bridge",
|
||||
"До 5 устройств с одним и тем же адресом": "Up to 5 devices sharing the same address",
|
||||
"Два типа подключения: WireGuard и Xray": "Two connection types: WireGuard and Xray",
|
||||
"Умная маршрутизация RU / Глобал без переключений": "Smart RU / global routing with no switching",
|
||||
"Где-то сервер уже стоит и доступ выдаётся сразу. Если свободного нет, мы устанавливаем сервер лично под вас — на это уходит до 24 часов, и вы видите этот статус честно, до оплаты.": "In some countries a server is already running and access is issued immediately. If none is free, we set one up for you personally — that takes up to 24 hours, and you see this status honestly, before paying.",
|
||||
"Сервер свободен — доступ выдаём сразу после оплаты": "A server is free — access is issued right after payment",
|
||||
"Свободного сервера нет: установим лично под вас, доступ в течение 24 часов": "No free server: we will set one up for you personally, access within 24 hours",
|
||||
"4 900 ₽ · далее 2 499 ₽": "$49 · then $24",
|
||||
"Нужно несколько локаций": "Need several locations",
|
||||
"Каждый тариф — отдельный индивидуальный IP. Берёте второй тариф в другой стране, оба ключа лежат в одном кабинете и продлеваются независимо.": "Each plan is a separate dedicated IP. Take a second plan in another country and both keys live in one account, renewing independently.",
|
||||
"Команда, много адресов, редкая страна или свой сценарий маршрутизации. Оставьте заявку — ответим в течение дня.": "A team, many addresses, a rare country or your own routing scenario. Leave a request and we will reply within a day.",
|
||||
"Оставить заявку": "Send a request",
|
||||
"Почта или @username в Telegram": "Email or @username on Telegram",
|
||||
"Коротко о задаче": "Briefly about your case",
|
||||
"Отправить": "Send",
|
||||
"Заявка отправлена": "Request sent",
|
||||
"Общий сервер": "Shared server",
|
||||
"Персональный IP": "Dedicated IP",
|
||||
"Общий сервер, адрес делится с другими пользователями": "A shared server: the address is shared with other users",
|
||||
"в месяц": "per month",
|
||||
"или 4 900 ₽ за год": "or $49 per year",
|
||||
"Подключение сразу после оплаты: сервер уже работает, вы просто получаете доступ. Отменить можно в кабинете.": "You connect right after payment: the server is already running, you simply get access. You can cancel in your account.",
|
||||
"Общий IP в выбранной стране": "Shared IP in the country you choose",
|
||||
"Доступ сразу после оплаты": "Access right after payment",
|
||||
"До 5 устройств": "Up to 5 devices",
|
||||
"Общие серверы уже работают во всех странах из списка — доступ выдаём сразу после оплаты.": "Shared servers are already running in every country on the list — access is issued right after payment.",
|
||||
"490 ₽ в месяц": "$5 per month",
|
||||
"Чем персональный IP лучше общего": "Why a dedicated IP beats a shared one",
|
||||
"На общем сервере адрес делят другие пользователи, и ограничения, которые получил любой из них, касаются всех. Персональный IP принадлежит только вам — сервисы относятся к нему как к домашнему интернету.": "On a shared server the address is used by other people, and any restriction one of them earns applies to everyone. A dedicated IP belongs to you alone — services treat it like a home connection.",
|
||||
|
||||
// Поддержка
|
||||
"Быстрее всего. Можно писать голосом и скидывать скриншоты.": "The fastest way. Voice messages and screenshots welcome.",
|
||||
"Почта": "Email",
|
||||
"Для счетов, возвратов и всего, что нужно письменно.": "For invoices, refunds and anything that needs to be in writing.",
|
||||
"Частые вопросы": "Frequent questions",
|
||||
"Подключение": "Connecting",
|
||||
"Оплата и продление": "Payment and renewal",
|
||||
"Локации и IP": "Locations and IP",
|
||||
"Что делать сразу после оплаты?": "What do I do right after paying?",
|
||||
"Зайдите в кабинет по почте, откройте свой ключ и скачайте конфигурацию или отсканируйте QR-код с телефона. Маршрутизация уже настроена, вручную ничего вписывать не нужно.": "Sign in to your account by email, open your key and download the configuration or scan the QR code with your phone. Routing is already configured, nothing to type in by hand.",
|
||||
"До пяти на один ключ, все с одним и тем же адресом. Если нужно больше устройств или вторая страна — берёте дополнительный тариф с отдельным индивидуальным IP.": "Up to five per key, all sharing the same address. If you need more devices or a second country, take an additional plan with its own dedicated IP.",
|
||||
"Перестал работать один способ подключения.": "One connection method stopped working.",
|
||||
"На вашем сервере всегда настроены оба типа: WireGuard и Xray. Откройте кабинет и подключитесь по второй конфигурации, ничего запрашивать не нужно. Набор протоколов мы дополняем и обновляем.": "Both types are always configured on your server: WireGuard and Xray. Open your account and connect with the second configuration, no request needed. We keep extending and updating the set of protocols.",
|
||||
"Нужно ли что-то настраивать вручную?": "Do I need to configure anything manually?",
|
||||
"Нет. В конфигурации уже прописано, что российские сайты идут напрямую, а зарубежные — через ваш сервер. Переключать режимы во время работы не нужно.": "No. The configuration already states that Russian sites go direct and foreign ones go through your server. There are no modes to switch while you work.",
|
||||
"Почему первый месяц дороже?": "Why is the first month more expensive?",
|
||||
"В 4 900 ₽ входит подготовка сервера: выделение адреса, настройка обоих протоколов и маршрутизации. Дальше вы платите только абонентскую плату 2 499 ₽ в месяц.": "The $49 covers preparing the server: assigning the address and configuring both protocols and the routing. After that you only pay the $24 monthly fee.",
|
||||
"Как продлевается ключ?": "How does renewal work?",
|
||||
"Списание раз в месяц с привязанной карты. Дату окончания и карту видно на странице ключа в кабинете, там же оплату можно поменять или остановить.": "A monthly charge to the card on file. The expiry date and the card are shown on the key page in your account, where you can change or stop the payment.",
|
||||
"Можно вернуть деньги?": "Can I get a refund?",
|
||||
"Если сервер не заработал по нашей вине, возвращаем полностью в течение первых семи дней. Если вы просто передумали — возвращаем остаток неиспользованного месяца.": "If the server failed to work through our fault, we refund in full within the first seven days. If you simply changed your mind, we refund the unused part of the month.",
|
||||
"Чем можно платить?": "What can I pay with?",
|
||||
"Карты российских и зарубежных банков, а также криптовалюта. Все способы доступны на странице оформления заказа.": "Cards from Russian and foreign banks, as well as cryptocurrency. All methods are available at checkout.",
|
||||
"Чем индивидуальный IP отличается от обычного VPN?": "How does a dedicated IP differ from an ordinary VPN?",
|
||||
"В массовом VPN один адрес делят сотни людей, и ограничения, которые получил любой из них, касаются всех. Ваш адрес принадлежит только вам, поэтому сервисы относятся к нему как к домашнему интернету.": "In a mass-market VPN hundreds of people share one address, and any restriction one of them earns applies to everyone. Your address belongs to you alone, so services treat it like a home connection.",
|
||||
"Почему часть локаций готова только через 24 часа?": "Why are some locations only ready in 24 hours?",
|
||||
"Мы не держим пул общих адресов. Если в нужной стране свободного сервера нет, мы устанавливаем его лично под вас. Оплата первого месяца начинается с момента выдачи доступа.": "We do not keep a pool of shared addresses. If there is no free server in the country you need, we set one up for you personally. The first month starts when access is issued.",
|
||||
"Зачем к тарифу российский адрес?": "Why does the plan include a Russian address?",
|
||||
"Можно сменить страну позже?": "Can I change the country later?",
|
||||
"Ключ привязан к своему серверу, поэтому страну у него не меняем. Если нужна другая локация, возьмите второй ключ: он появится в том же кабинете и будет продлеваться отдельно. Первый можно оставить или отключить в любой момент.": "A key is tied to its own server, so we don't change its country. If you need another location, take a second key: it appears in the same account and renews separately. Keep the first one or cancel it whenever you like.",
|
||||
"Написать нам": "Write to us",
|
||||
"Если уже купили ключ, укажите почту, на которую он оформлен — так мы сразу увидим вашу конфигурацию.": "If you already bought a key, use the email it is registered to so we can see your configuration straight away.",
|
||||
"Ваша почта": "Your email",
|
||||
"Тема: подключение": "Topic: connecting",
|
||||
"Тема: оплата и продление": "Topic: payment and renewal",
|
||||
"Тема: локации и IP": "Topic: locations and IP",
|
||||
"Тема: другое": "Topic: something else",
|
||||
"Что произошло": "What happened",
|
||||
"Отправлено, ответим на почту": "Sent, we will reply by email",
|
||||
"Выберите удобный способ связи.": "Choose whichever way suits you.",
|
||||
|
||||
// Оформление заказа
|
||||
"Оформление заказа": "Checkout",
|
||||
"Оплата": "Payment",
|
||||
"Изменить": "Change",
|
||||
"Введите почту — пришлём одноразовый код. Отдельной регистрации нет: если вы у нас уже были, попадёте в свой кабинет, если нет — он создастся сам.": "Enter your email and we will send a one-time code. There is no separate sign-up: if you have been here before you land in your account, if not it is created for you.",
|
||||
"Прислать код": "Send the code",
|
||||
"6 цифр": "6 digits",
|
||||
"Продолжить": "Continue",
|
||||
"Другая почта": "Use another email",
|
||||
"Карта": "Card",
|
||||
"Карта МИР": "MIR card",
|
||||
"Криптовалюта": "Cryptocurrency",
|
||||
"Номер карты": "Card number",
|
||||
"ММ / ГГ": "MM / YY",
|
||||
"Карта сохранится для продления. Отменить или заменить её можно в кабинете в любой момент.": "The card is saved for renewals. You can cancel or replace it in your account at any time.",
|
||||
"USDT, BTC или ETH": "USDT, BTC or ETH",
|
||||
"После нажатия покажем адрес и сумму. При оплате криптой продление не автоматическое — пришлём напоминание за три дня до конца месяца.": "We will show the address and amount after you press the button. With crypto, renewal is not automatic — we send a reminder three days before the month ends.",
|
||||
"Согласен с условиями использования и политикой конфиденциальности": "I agree to the terms of use and the privacy policy",
|
||||
"Ваш заказ": "Your order",
|
||||
"Персональный VPN": "Personal VPN",
|
||||
"Сервер свободен, доступ сразу после оплаты": "A server is free, access right after payment",
|
||||
"Первый месяц": "First month",
|
||||
"RU-адрес": "RU address",
|
||||
"включён": "included",
|
||||
"Устройств": "Devices",
|
||||
"до 5": "up to 5",
|
||||
"К оплате сегодня": "Due today",
|
||||
"Оплатить 4 900 ₽": "Pay $49",
|
||||
"Показать адрес для оплаты": "Show the payment address",
|
||||
"Оплачено": "Paid",
|
||||
"Оплачено. Ключ уже в кабинете —": "Paid. The key is already in your account —",
|
||||
"открыть": "open it",
|
||||
"Возврат в течение семи дней, если сервер не заработал по нашей вине. Вопросы —": "A refund within seven days if the server failed through our fault. Questions —",
|
||||
"поддержка": "support",
|
||||
|
||||
// Кабинет
|
||||
"Вход в кабинет": "Sign in",
|
||||
"Введите почту, пришлём одноразовый код. Отдельной регистрации нет — кабинет создастся сам, если вы здесь впервые.": "Enter your email and we will send a one-time code. There is no separate sign-up — the account is created for you if this is your first visit.",
|
||||
"Главная": "Overview",
|
||||
"Список ключей": "All keys",
|
||||
"Купить ключ": "Buy a key",
|
||||
"Ваши ключи": "Your keys",
|
||||
"Два активных тарифа, каждый со своим индивидуальным IP.": "Two active plans, each with its own dedicated IP.",
|
||||
"Действует до": "Active until",
|
||||
"Активен": "Active",
|
||||
"Истекает": "Expiring",
|
||||
"3 из 5 устройств": "3 of 5 devices",
|
||||
"1 из 5 устройств": "1 of 5 devices",
|
||||
"Карта ···· 4242": "Card ···· 4242",
|
||||
"Карта ···· 8801": "Card ···· 8801",
|
||||
"Нужен ещё один IP или другая страна": "Need another IP or a different country",
|
||||
"Новый ключ появится здесь же, оплата отдельная.": "The new key appears right here, billed separately.",
|
||||
"Короткие ответы": "Quick answers",
|
||||
"Как подключить новое устройство?": "How do I connect a new device?",
|
||||
"Откройте ключ, скачайте любую свободную конфигурацию WireGuard или отсканируйте её QR-код. Каждая конфигурация рассчитана на одно устройство.": "Open the key, download any free WireGuard configuration or scan its QR code. Each configuration is meant for one device.",
|
||||
"Что делать, если подключение перестало работать?": "What if the connection stops working?",
|
||||
"Переключитесь на Xray в том же ключе — он настроен заранее. Если не помогло, напишите в поддержку, ответим в течение часа.": "Switch to Xray within the same key — it is configured in advance. If that does not help, write to support and we will reply within an hour.",
|
||||
"Как отменить продление?": "How do I cancel renewal?",
|
||||
"На странице ключа нажмите «Изменить карту» и отвяжите её. Ключ доработает оплаченный месяц.": "On the key page press “Change card” and detach it. The key keeps working until the paid month ends.",
|
||||
"← Все ключи": "← All keys",
|
||||
"Изменить карту": "Change card",
|
||||
"2 499 ₽ в месяц": "$24 per month",
|
||||
"Устройства": "Devices",
|
||||
"Один и тот же IP на всех": "The same IP on all of them",
|
||||
"Продлится автоматически через 31 день": "Renews automatically in 31 days",
|
||||
"Осталось 8 дней": "8 days left",
|
||||
"5 конфигураций — по одной на устройство": "5 configurations — one per device",
|
||||
"На компьютере скачайте файл и откройте его в приложении. На телефоне — отсканируйте QR-код.": "On a computer, download the file and open it in the app. On a phone, scan the QR code.",
|
||||
"Ноутбук": "Laptop",
|
||||
"Телефон": "Phone",
|
||||
"Планшет": "Tablet",
|
||||
"Рабочий ноутбук": "Work laptop",
|
||||
"Свободный слот": "Free slot",
|
||||
"подключено": "connected",
|
||||
"не занят": "unused",
|
||||
"Скачать": "Download",
|
||||
"Для Happ и других клиентов": "For Happ and other clients",
|
||||
"Запасной способ подключения. Скопируйте ссылку или отсканируйте код — настройки те же.": "A backup way to connect. Copy the link or scan the code — the settings are the same.",
|
||||
"Копировать": "Copy",
|
||||
"Копировать ключ": "Copy key",
|
||||
"Скопировано": "Copied",
|
||||
"Цена одна для любой страны: 4 900 ₽ первый месяц, далее 2 499 ₽.": "One price for every country: $49 the first month, then $24.",
|
||||
"Доступ выдадим сразу после оплаты": "Access is issued right after payment",
|
||||
"Установим сервер под вас, доступ в течение 24 часов": "We will set up a server for you, access within 24 hours",
|
||||
"Откройте приложение на телефоне и наведите камеру на код.": "Open the app on your phone and point the camera at the code.",
|
||||
"Выйти · anna@example.com": "Sign out · anna@example.com",
|
||||
"Светлая тема": "Light theme",
|
||||
"Тёмная тема": "Dark theme",
|
||||
"Как в системе": "Match the system",
|
||||
"Язык: русский": "Language: Russian",
|
||||
"Язык: английский": "Language: English",
|
||||
|
||||
// MiniApp
|
||||
"мини-приложение": "mini app",
|
||||
"Закрыть": "Close",
|
||||
"Нажмите, чтобы открыть подключение": "Tap to open the connection",
|
||||
"Купить ещё один ключ": "Buy another key",
|
||||
"← Ключи": "← Keys",
|
||||
"Ключи": "Keys",
|
||||
"Ещё": "More",
|
||||
"5 устройств": "5 devices",
|
||||
"Файл": "File",
|
||||
"Happ и другие": "Happ and others",
|
||||
"4 900 ₽ первый месяц, далее 2 499 ₽": "$49 first month, then $24",
|
||||
"Оплата пройдёт внутри Telegram, ключ появится в списке.": "Payment happens inside Telegram, the key appears in the list.",
|
||||
"Написать в поддержку": "Message support",
|
||||
"Почта аккаунта": "Account email",
|
||||
"По умолчанию подстраивается под тему Telegram.": "By default it follows the Telegram theme.",
|
||||
"Наведите камеру телефона на код в приложении WireGuard или Happ.": "Point your phone camera at the code in WireGuard or Happ.",
|
||||
"Мини-приложение в Telegram": "Telegram mini app",
|
||||
"Тот же кабинет в парадигме телефона: список ключей, страница ключа с QR-кодами и покупка без выхода в браузер. Вход не нужен — Telegram уже знает, кто вы.": "The same account in phone form: the list of keys, a key page with QR codes and buying without leaving Telegram. No sign-in needed — Telegram already knows who you are.",
|
||||
"Нижние вкладки повторяют сайдбар веб-кабинета: Ключи, Купить, Ещё.": "The bottom tabs mirror the web sidebar: Keys, Buy, More.",
|
||||
|
||||
// Локации — 30 направлений
|
||||
"Великобритания": "United Kingdom",
|
||||
"Лондон": "London",
|
||||
"Франция": "France",
|
||||
"Париж": "Paris",
|
||||
"Швеция": "Sweden",
|
||||
"Стокгольм": "Stockholm",
|
||||
"Норвегия": "Norway",
|
||||
"Осло": "Oslo",
|
||||
"Дания": "Denmark",
|
||||
"Копенгаген": "Copenhagen",
|
||||
"Швейцария": "Switzerland",
|
||||
"Цюрих": "Zurich",
|
||||
"Австрия": "Austria",
|
||||
"Вена": "Vienna",
|
||||
"Испания": "Spain",
|
||||
"Мадрид": "Madrid",
|
||||
"Италия": "Italy",
|
||||
"Милан": "Milan",
|
||||
"Польша": "Poland",
|
||||
"Варшава": "Warsaw",
|
||||
"Чехия": "Czechia",
|
||||
"Прага": "Prague",
|
||||
"Румыния": "Romania",
|
||||
"Бухарест": "Bucharest",
|
||||
"Литва": "Lithuania",
|
||||
"Вильнюс": "Vilnius",
|
||||
"Латвия": "Latvia",
|
||||
"Рига": "Riga",
|
||||
"Эстония": "Estonia",
|
||||
"Таллин": "Tallinn",
|
||||
"Казахстан": "Kazakhstan",
|
||||
"Алматы": "Almaty",
|
||||
"Армения": "Armenia",
|
||||
"Ереван": "Yerevan",
|
||||
"Грузия": "Georgia",
|
||||
"Тбилиси": "Tbilisi",
|
||||
"ОАЭ": "UAE",
|
||||
"Дубай": "Dubai",
|
||||
"Сингапур": "Singapore",
|
||||
"Южная Корея": "South Korea",
|
||||
"Сеул": "Seoul",
|
||||
"Канада": "Canada",
|
||||
"Торонто": "Toronto",
|
||||
"Австралия": "Australia",
|
||||
"Сидней": "Sydney",
|
||||
"Бразилия": "Brazil",
|
||||
"Сан-Паулу": "São Paulo",
|
||||
|
||||
// Цены локаций
|
||||
"6 900 ₽": "$69",
|
||||
"3 499 ₽": "$34",
|
||||
"9 900 ₽": "$99",
|
||||
"4 999 ₽": "$49",
|
||||
"первый месяц · далее": "first month · then",
|
||||
"далее": "then",
|
||||
"в месяц": "per month",
|
||||
"Персональный сервер с индивидуальным IP": "A personal server with a dedicated IP",
|
||||
"30 стран на выбор и российский адрес в комплекте с любым тарифом.": "30 countries to choose from, with a Russian address included in every plan.",
|
||||
"Базовая цена — 4 900 ₽ первый месяц и 2 499 ₽ далее. В редких локациях она выше и указана на карточке.": "The base price is $49 for the first month and $24 after that. Rare locations cost more, and the price is shown on the card.",
|
||||
"Нажмите точку на карте — покажем страну и цену": "Tap a dot on the map to see the country and its price",
|
||||
"Базовая цена: 4 900 ₽ первый месяц, далее 2 499 ₽. В редких локациях — выше.": "Base price: $49 the first month, then $24. Rare locations cost more.",
|
||||
|
||||
"Имя": "Name",
|
||||
"Нужны индивидуальные условия": "Need custom terms",
|
||||
|
||||
"Отвечаем с 10 до 20 в рабочие дни.": "We reply from 10:00 to 20:00 on business days.",
|
||||
|
||||
"Нужной страны нет в списке, нужно много адресов или свой сценарий маршрутизации — напишите, скажем сроки и установим под вас.": "If your country is missing, you need many addresses, or you have a custom routing setup — write to us and we will give you timing and set it up.",
|
||||
|
||||
// Даты и единицы, встречающиеся в демо-данных
|
||||
"14 октября 2026": "14 October 2026",
|
||||
"21 сентября 2026": "21 September 2026",
|
||||
"14 октября": "14 October",
|
||||
"21 сентября": "21 September"
|
||||
};
|
||||
|
||||
// Переключение языка: подменяет текстовые узлы и подписи внутри корня.
|
||||
window.VEILIX_APPLY_LANG = function (root, lang) {
|
||||
const dict = window.VEILIX_I18N || {};
|
||||
const el = root || document.body;
|
||||
if (!el) return;
|
||||
const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT, null);
|
||||
const nodes = [];
|
||||
while (walker.nextNode()) nodes.push(walker.currentNode);
|
||||
nodes.forEach(node => {
|
||||
const parent = node.parentNode;
|
||||
if (parent && (parent.nodeName === "SCRIPT" || parent.nodeName === "STYLE")) return;
|
||||
// Кэш валиден, только если узел с тех пор не менялся снаружи (React мог перерисовать текст).
|
||||
const cached = node.__veilixOut != null && node.nodeValue === node.__veilixOut;
|
||||
if (lang === "en") {
|
||||
const raw = cached ? node.__veilixRu : node.nodeValue;
|
||||
const key = raw.trim();
|
||||
if (!key || !dict[key]) { node.__veilixRu = null; node.__veilixOut = null; return; }
|
||||
node.__veilixRu = raw;
|
||||
node.nodeValue = raw.replace(key, dict[key]);
|
||||
node.__veilixOut = node.nodeValue;
|
||||
} else if (cached && node.__veilixRu != null) {
|
||||
node.nodeValue = node.__veilixRu;
|
||||
node.__veilixRu = null;
|
||||
node.__veilixOut = null;
|
||||
}
|
||||
});
|
||||
["placeholder", "title", "aria-label"].forEach(attr => {
|
||||
el.querySelectorAll("[" + attr + "]").forEach(target => {
|
||||
const store = "__veilix_" + attr;
|
||||
const out = "__veilixOut_" + attr;
|
||||
const current = target.getAttribute(attr);
|
||||
const cached = target[out] != null && current === target[out];
|
||||
if (lang === "en") {
|
||||
const raw = cached ? target[store] : current;
|
||||
const key = (raw || "").trim();
|
||||
if (!key || !dict[key]) { target[store] = null; target[out] = null; return; }
|
||||
target[store] = raw;
|
||||
target.setAttribute(attr, dict[key]);
|
||||
target[out] = dict[key];
|
||||
} else if (cached && target[store] != null) {
|
||||
target.setAttribute(attr, target[store]);
|
||||
target[store] = null;
|
||||
target[out] = null;
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,352 @@
|
||||
// @ds-adherence-ignore -- omelette starter scaffold (raw elements/hex/px by design)
|
||||
// Copied omelette starter. Re-running copy_starter_component with this kind overwrites this file with the latest version (page content is unaffected).
|
||||
|
||||
/* BEGIN USAGE */
|
||||
// iOS.jsx — Simplified iOS 26 (Liquid Glass) device frame
|
||||
// Based on the iOS 26 UI Kit + Figma status bar spec. No assets, no deps.
|
||||
// Exports (to window): IOSDevice, IOSStatusBar, IOSNavBar, IOSGlassPill, IOSList, IOSListRow, IOSKeyboard
|
||||
//
|
||||
// Usage — wrap your screen content in <IOSDevice> to get the bezel, status bar
|
||||
// and home indicator (props: title, dark, keyboard):
|
||||
//
|
||||
// <IOSDevice title="Settings">
|
||||
// ...your screen content...
|
||||
// </IOSDevice>
|
||||
// <IOSDevice dark title="Search" keyboard>…</IOSDevice>
|
||||
/* END USAGE */
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// Status bar
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
function IOSStatusBar({ dark = false, time = '9:41' }) {
|
||||
const c = dark ? '#fff' : '#000';
|
||||
return (
|
||||
<div style={{
|
||||
display: 'flex', gap: 154, alignItems: 'center', justifyContent: 'center',
|
||||
padding: '21px 24px 19px', boxSizing: 'border-box',
|
||||
position: 'relative', zIndex: 20, width: '100%',
|
||||
}}>
|
||||
<div style={{ flex: 1, height: 22, display: 'flex', alignItems: 'center', justifyContent: 'center', paddingTop: 1.5 }}>
|
||||
<span style={{
|
||||
fontFamily: '-apple-system, "SF Pro", system-ui', fontWeight: 590,
|
||||
fontSize: 17, lineHeight: '22px', color: c,
|
||||
}}>{time}</span>
|
||||
</div>
|
||||
<div style={{ flex: 1, height: 22, display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 7, paddingTop: 1, paddingRight: 1 }}>
|
||||
<svg width="19" height="12" viewBox="0 0 19 12">
|
||||
<rect x="0" y="7.5" width="3.2" height="4.5" rx="0.7" fill={c}/>
|
||||
<rect x="4.8" y="5" width="3.2" height="7" rx="0.7" fill={c}/>
|
||||
<rect x="9.6" y="2.5" width="3.2" height="9.5" rx="0.7" fill={c}/>
|
||||
<rect x="14.4" y="0" width="3.2" height="12" rx="0.7" fill={c}/>
|
||||
</svg>
|
||||
<svg width="17" height="12" viewBox="0 0 17 12">
|
||||
<path d="M8.5 3.2C10.8 3.2 12.9 4.1 14.4 5.6L15.5 4.5C13.7 2.7 11.2 1.5 8.5 1.5C5.8 1.5 3.3 2.7 1.5 4.5L2.6 5.6C4.1 4.1 6.2 3.2 8.5 3.2Z" fill={c}/>
|
||||
<path d="M8.5 6.8C9.9 6.8 11.1 7.3 12 8.2L13.1 7.1C11.8 5.9 10.2 5.1 8.5 5.1C6.8 5.1 5.2 5.9 3.9 7.1L5 8.2C5.9 7.3 7.1 6.8 8.5 6.8Z" fill={c}/>
|
||||
<circle cx="8.5" cy="10.5" r="1.5" fill={c}/>
|
||||
</svg>
|
||||
<svg width="27" height="13" viewBox="0 0 27 13">
|
||||
<rect x="0.5" y="0.5" width="23" height="12" rx="3.5" stroke={c} strokeOpacity="0.35" fill="none"/>
|
||||
<rect x="2" y="2" width="20" height="9" rx="2" fill={c}/>
|
||||
<path d="M25 4.5V8.5C25.8 8.2 26.5 7.2 26.5 6.5C26.5 5.8 25.8 4.8 25 4.5Z" fill={c} fillOpacity="0.4"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// Liquid glass pill — blur + tint + shine
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
function IOSGlassPill({ children, dark = false, style = {} }) {
|
||||
return (
|
||||
<div style={{
|
||||
height: 44, minWidth: 44, borderRadius: 9999,
|
||||
position: 'relative', overflow: 'hidden',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
boxShadow: dark
|
||||
? '0 2px 6px rgba(0,0,0,0.35), 0 6px 16px rgba(0,0,0,0.2)'
|
||||
: '0 1px 3px rgba(0,0,0,0.07), 0 3px 10px rgba(0,0,0,0.06)',
|
||||
...style,
|
||||
}}>
|
||||
{/* blur + tint */}
|
||||
<div style={{
|
||||
position: 'absolute', inset: 0, borderRadius: 9999,
|
||||
backdropFilter: 'blur(12px) saturate(180%)',
|
||||
WebkitBackdropFilter: 'blur(12px) saturate(180%)',
|
||||
background: dark ? 'rgba(120,120,128,0.28)' : 'rgba(255,255,255,0.5)',
|
||||
}} />
|
||||
{/* shine */}
|
||||
<div style={{
|
||||
position: 'absolute', inset: 0, borderRadius: 9999,
|
||||
boxShadow: dark
|
||||
? 'inset 1.5px 1.5px 1px rgba(255,255,255,0.15), inset -1px -1px 1px rgba(255,255,255,0.08)'
|
||||
: 'inset 1.5px 1.5px 1px rgba(255,255,255,0.7), inset -1px -1px 1px rgba(255,255,255,0.4)',
|
||||
border: dark ? '0.5px solid rgba(255,255,255,0.15)' : '0.5px solid rgba(0,0,0,0.06)',
|
||||
}} />
|
||||
<div style={{ position: 'relative', zIndex: 1, display: 'flex', alignItems: 'center', padding: '0 4px' }}>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// Navigation bar — glass pills + large title
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
function IOSNavBar({ title = 'Title', dark = false, trailingIcon = true }) {
|
||||
const muted = dark ? 'rgba(255,255,255,0.6)' : '#404040';
|
||||
const text = dark ? '#fff' : '#000';
|
||||
const pillIcon = (content) => (
|
||||
<IOSGlassPill dark={dark}>
|
||||
<div style={{ width: 36, height: 36, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
{content}
|
||||
</div>
|
||||
</IOSGlassPill>
|
||||
);
|
||||
return (
|
||||
<div style={{
|
||||
display: 'flex', flexDirection: 'column', gap: 10,
|
||||
paddingTop: 62, paddingBottom: 10, position: 'relative', zIndex: 5,
|
||||
}}>
|
||||
<div style={{
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
||||
padding: '0 16px',
|
||||
}}>
|
||||
{/* back chevron */}
|
||||
{pillIcon(
|
||||
<svg width="12" height="20" viewBox="0 0 12 20" fill="none" style={{ marginLeft: -1 }}>
|
||||
<path d="M10 2L2 10l8 8" stroke={muted} strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
)}
|
||||
{/* trailing ellipsis */}
|
||||
{trailingIcon && pillIcon(
|
||||
<svg width="22" height="6" viewBox="0 0 22 6">
|
||||
<circle cx="3" cy="3" r="2.5" fill={muted}/>
|
||||
<circle cx="11" cy="3" r="2.5" fill={muted}/>
|
||||
<circle cx="19" cy="3" r="2.5" fill={muted}/>
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
{/* large title */}
|
||||
<div style={{
|
||||
padding: '0 16px',
|
||||
fontFamily: '-apple-system, system-ui',
|
||||
fontSize: 34, fontWeight: 700, lineHeight: '41px',
|
||||
color: text, letterSpacing: 0.4,
|
||||
}}>{title}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// Grouped list (inset card, r:26) + row (52px)
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
function IOSListRow({ title, detail, icon, chevron = true, isLast = false, dark = false }) {
|
||||
const text = dark ? '#fff' : '#000';
|
||||
const sec = dark ? 'rgba(235,235,245,0.6)' : 'rgba(60,60,67,0.6)';
|
||||
const ter = dark ? 'rgba(235,235,245,0.3)' : 'rgba(60,60,67,0.3)';
|
||||
const sep = dark ? 'rgba(84,84,88,0.65)' : 'rgba(60,60,67,0.12)';
|
||||
return (
|
||||
<div style={{
|
||||
display: 'flex', alignItems: 'center', minHeight: 52,
|
||||
padding: '0 16px', position: 'relative',
|
||||
fontFamily: '-apple-system, system-ui', fontSize: 17,
|
||||
letterSpacing: -0.43,
|
||||
}}>
|
||||
{icon && (
|
||||
<div style={{
|
||||
width: 30, height: 30, borderRadius: 7, background: icon,
|
||||
marginRight: 12, flexShrink: 0,
|
||||
}} />
|
||||
)}
|
||||
<div style={{ flex: 1, color: text }}>{title}</div>
|
||||
{detail && <span style={{ color: sec, marginRight: 6 }}>{detail}</span>}
|
||||
{chevron && (
|
||||
<svg width="8" height="14" viewBox="0 0 8 14" style={{ flexShrink: 0 }}>
|
||||
<path d="M1 1l6 6-6 6" stroke={ter} strokeWidth="2" fill="none" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
)}
|
||||
{!isLast && (
|
||||
<div style={{
|
||||
position: 'absolute', bottom: 0, right: 0,
|
||||
left: icon ? 58 : 16, height: 0.5, background: sep,
|
||||
}} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function IOSList({ header, children, dark = false }) {
|
||||
const hc = dark ? 'rgba(235,235,245,0.6)' : 'rgba(60,60,67,0.6)';
|
||||
const bg = dark ? '#1C1C1E' : '#fff';
|
||||
return (
|
||||
<div>
|
||||
{header && (
|
||||
<div style={{
|
||||
fontFamily: '-apple-system, system-ui', fontSize: 13,
|
||||
color: hc, textTransform: 'uppercase',
|
||||
padding: '8px 36px 6px', letterSpacing: -0.08,
|
||||
}}>{header}</div>
|
||||
)}
|
||||
<div style={{
|
||||
background: bg, borderRadius: 26,
|
||||
margin: '0 16px', overflow: 'hidden',
|
||||
}}>{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// Device frame
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
function IOSDevice({
|
||||
children, width = 402, height = 874, dark = false,
|
||||
title, keyboard = false,
|
||||
}) {
|
||||
return (
|
||||
// data-om-starter: inert presence marker — Claude Design's starter-usage
|
||||
// probe reads it; it renders nothing. Keep it on this root element.
|
||||
<div data-om-starter="ios-frame" style={{
|
||||
width, height, borderRadius: 48, overflow: 'hidden',
|
||||
position: 'relative', background: dark ? '#000' : '#F2F2F7',
|
||||
boxShadow: '0 40px 80px rgba(0,0,0,0.18), 0 0 0 1px rgba(0,0,0,0.12)',
|
||||
fontFamily: '-apple-system, system-ui, sans-serif',
|
||||
WebkitFontSmoothing: 'antialiased',
|
||||
}}>
|
||||
{/* dynamic island */}
|
||||
<div style={{
|
||||
position: 'absolute', top: 11, left: '50%', transform: 'translateX(-50%)',
|
||||
width: 126, height: 37, borderRadius: 24, background: '#000', zIndex: 50,
|
||||
}} />
|
||||
{/* status bar (absolute) */}
|
||||
<div style={{ position: 'absolute', top: 0, left: 0, right: 0, zIndex: 10 }}>
|
||||
<IOSStatusBar dark={dark} />
|
||||
</div>
|
||||
{/* nav + content */}
|
||||
<div style={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
|
||||
{title !== undefined && <IOSNavBar title={title} dark={dark} />}
|
||||
<div style={{ flex: 1, overflow: 'auto' }}>{children}</div>
|
||||
{keyboard && <IOSKeyboard dark={dark} />}
|
||||
</div>
|
||||
{/* home indicator — always on top */}
|
||||
<div style={{
|
||||
position: 'absolute', bottom: 0, left: 0, right: 0, zIndex: 60,
|
||||
height: 34, display: 'flex', justifyContent: 'center', alignItems: 'flex-end',
|
||||
paddingBottom: 8, pointerEvents: 'none',
|
||||
}}>
|
||||
<div style={{
|
||||
width: 139, height: 5, borderRadius: 100,
|
||||
background: dark ? 'rgba(255,255,255,0.7)' : 'rgba(0,0,0,0.25)',
|
||||
}} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// Keyboard — iOS 26 liquid glass
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
function IOSKeyboard({ dark = false }) {
|
||||
const glyph = dark ? 'rgba(255,255,255,0.7)' : '#595959';
|
||||
const sugg = dark ? 'rgba(255,255,255,0.6)' : '#333';
|
||||
const keyBg = dark ? 'rgba(255,255,255,0.22)' : 'rgba(255,255,255,0.85)';
|
||||
|
||||
// special-key icons
|
||||
const icons = {
|
||||
shift: <svg width="19" height="17" viewBox="0 0 19 17"><path d="M9.5 1L1 9.5h4.5V16h8V9.5H18L9.5 1z" fill={glyph}/></svg>,
|
||||
del: <svg width="23" height="17" viewBox="0 0 23 17"><path d="M7 1h13a2 2 0 012 2v11a2 2 0 01-2 2H7l-6-7.5L7 1z" fill="none" stroke={glyph} strokeWidth="1.6" strokeLinejoin="round"/><path d="M10 5l7 7M17 5l-7 7" stroke={glyph} strokeWidth="1.6" strokeLinecap="round"/></svg>,
|
||||
ret: <svg width="20" height="14" viewBox="0 0 20 14"><path d="M18 1v6H4m0 0l4-4M4 7l4 4" fill="none" stroke="#fff" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"/></svg>,
|
||||
};
|
||||
|
||||
const key = (content, { w, flex, ret, fs = 25, k } = {}) => (
|
||||
<div key={k} style={{
|
||||
height: 42, borderRadius: 8.5,
|
||||
flex: flex ? 1 : undefined, width: w, minWidth: 0,
|
||||
background: ret ? '#08f' : keyBg,
|
||||
boxShadow: '0 1px 0 rgba(0,0,0,0.075)',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
fontFamily: '-apple-system, "SF Compact", system-ui',
|
||||
fontSize: fs, fontWeight: 458, color: ret ? '#fff' : glyph,
|
||||
}}>{content}</div>
|
||||
);
|
||||
|
||||
const row = (keys, pad = 0) => (
|
||||
<div style={{ display: 'flex', gap: 6.5, justifyContent: 'center', padding: `0 ${pad}px` }}>
|
||||
{keys.map(l => key(l, { flex: true, k: l }))}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
position: 'relative', zIndex: 15, borderRadius: 27, overflow: 'hidden',
|
||||
padding: '11px 0 2px',
|
||||
display: 'flex', flexDirection: 'column', alignItems: 'center',
|
||||
boxShadow: dark
|
||||
? '0 -2px 20px rgba(0,0,0,0.09)'
|
||||
: '0 -1px 6px rgba(0,0,0,0.018), 0 -3px 20px rgba(0,0,0,0.012)',
|
||||
}}>
|
||||
{/* liquid glass bg — same recipe as nav pills */}
|
||||
<div style={{
|
||||
position: 'absolute', inset: 0, borderRadius: 27,
|
||||
backdropFilter: 'blur(12px) saturate(180%)',
|
||||
WebkitBackdropFilter: 'blur(12px) saturate(180%)',
|
||||
background: dark ? 'rgba(120,120,128,0.14)' : 'rgba(255,255,255,0.25)',
|
||||
}} />
|
||||
<div style={{
|
||||
position: 'absolute', inset: 0, borderRadius: 27,
|
||||
boxShadow: dark
|
||||
? 'inset 1.5px 1.5px 1px rgba(255,255,255,0.15)'
|
||||
: 'inset 1.5px 1.5px 1px rgba(255,255,255,0.7), inset -1px -1px 1px rgba(255,255,255,0.4)',
|
||||
border: dark ? '0.5px solid rgba(255,255,255,0.15)' : '0.5px solid rgba(0,0,0,0.06)',
|
||||
pointerEvents: 'none',
|
||||
}} />
|
||||
|
||||
{/* autocorrect bar */}
|
||||
<div style={{
|
||||
display: 'flex', gap: 20, alignItems: 'center',
|
||||
padding: '8px 22px 13px', width: '100%', boxSizing: 'border-box',
|
||||
position: 'relative',
|
||||
}}>
|
||||
{['"The"', 'the', 'to'].map((w, i) => (
|
||||
<React.Fragment key={i}>
|
||||
{i > 0 && <div style={{ width: 1, height: 25, background: '#ccc', opacity: 0.3 }} />}
|
||||
<div style={{
|
||||
flex: 1, textAlign: 'center',
|
||||
fontFamily: '-apple-system, system-ui', fontSize: 17,
|
||||
color: sugg, letterSpacing: -0.43, lineHeight: '22px',
|
||||
}}>{w}</div>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* key layout */}
|
||||
<div style={{
|
||||
display: 'flex', flexDirection: 'column', gap: 13,
|
||||
padding: '0 6.5px', width: '100%', boxSizing: 'border-box',
|
||||
position: 'relative',
|
||||
}}>
|
||||
{row(['q','w','e','r','t','y','u','i','o','p'])}
|
||||
{row(['a','s','d','f','g','h','j','k','l'], 20)}
|
||||
<div style={{ display: 'flex', gap: 14.25, alignItems: 'center' }}>
|
||||
{key(icons.shift, { w: 45, k: 'shift' })}
|
||||
<div style={{ display: 'flex', gap: 6.5, flex: 1 }}>
|
||||
{['z','x','c','v','b','n','m'].map(l => key(l, { flex: true, k: l }))}
|
||||
</div>
|
||||
{key(icons.del, { w: 45, k: 'del' })}
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 6, alignItems: 'center' }}>
|
||||
{key('ABC', { w: 92.25, fs: 18, k: 'abc' })}
|
||||
{key('', { flex: true, k: 'space' })}
|
||||
{key(icons.ret, { w: 92.25, ret: true, k: 'ret' })}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* bottom spacer (emoji+mic area, icons omitted) */}
|
||||
<div style={{ height: 56, width: '100%', position: 'relative' }} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Object.assign(window, {
|
||||
IOSDevice, IOSStatusBar, IOSNavBar, IOSGlassPill, IOSList, IOSListRow, IOSKeyboard,
|
||||
});
|
||||
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 1.2 MiB |
@@ -0,0 +1,197 @@
|
||||
# Дизайн VEILIX
|
||||
|
||||
|
||||
## Общая задача
|
||||
|
||||
Нужен дизайн сайта по продаже персональных VPN. Не классический VPN провайдер, когда много клиентов на 1 сервере, а когда мы делаем каждому клиенту индивидуальный IP.
|
||||
|
||||
Быстрый современный сайт на tailwind с максимальной скоростью под Google Page Speed. Стек который будем дальше использовать: payloadcms + Next.
|
||||
|
||||
|
||||
## Дизайн
|
||||
|
||||
"Чистый" современный дизайн без лишней маркетинговых бессмысленных фраз, которые есть у всех крупных. Нам нужно выделиться, чётким предложениям видным и понятным, по делу, а не придумывать того чего нет. Прям минимум, тк нет лишней информации.
|
||||
|
||||
Если сравнить карты Apple vs Карты Google. Apple в своих картах явно пошёл не биться в лоб с Google, тк это бессмысленно, а сделан совсем минимальный интерфейс, при том что у Google в целом, тоже понятно, но за счёт функционала интерфейс перегружен в любом случае. Этот же пример и про Apple Music.
|
||||
|
||||
### Основные тезисы:
|
||||
|
||||
- "Чистая" страница с минимум информации
|
||||
- Современный шрифт, не технический
|
||||
- Это сайт не для гиков, а для обычных людей. Графика, иконки, шрифты должны располагать и быть понятны человеку не компьютерному. Есть когда сайты выглядят больше как для гиков и не только по описанию и текстам, а потому какие шрифты, графика, иконки.
|
||||
- Не нужны банальные иконки всяких замков, защит и прочего
|
||||
- Ночная тема
|
||||
|
||||
|
||||
## Верхнее меню
|
||||
|
||||
1. Логотип (слева)
|
||||
2. далее отдельно немного от логотипа правее
|
||||
- Цены
|
||||
- Локации
|
||||
- Поддержка
|
||||
3. далее прижато справа:
|
||||
- Переключение языков (иконка)
|
||||
- Кнопка Login
|
||||
- Кнопка Get VPN
|
||||
|
||||
Название Veilix - без слова VPN для EN. Для русского Вейликс.
|
||||
|
||||
Висит всегда на экране, стандартно.
|
||||
|
||||
|
||||
## Нижнее меню
|
||||
|
||||
Меню, теже пункты что и вверху
|
||||
Дополнительно Legal страницы
|
||||
Оплата карточками
|
||||
Переключение темы 3 режима
|
||||
Переключение языка
|
||||
|
||||
|
||||
## Логотип
|
||||
|
||||
Придумать логотип лёгкий, тк Veilix, от слова Вуаль - Veil, когда она легко скрывает за собой что-то, нужен и логотип такой же. Те нужно чтобы это были какие-то замки и вот это всё банальное. У Nord на лого какие-то горы, возможно что-то с севером ассоциировано.
|
||||
|
||||
|
||||
|
||||
## Цвета
|
||||
|
||||
Сайту нужен светлый, например у Nord hero экран тёмный - так не нужно. У blancvpn тоже. При этом акцентные цвета - тёмно фиолетовый, глубокий и пастельный.
|
||||
|
||||
У Firefox режим инкогнито когда включается там набор градиентный фиолетовых цветов.
|
||||
|
||||
У Protonvpn в сторону Фиолетового неплохие цвета и светлое всё, нет тёмных.
|
||||
|
||||
|
||||
## Языки
|
||||
|
||||
Сделать всё на 2х языках RU и EN.
|
||||
|
||||
|
||||
|
||||
## Главная страница
|
||||
|
||||
### Hero экран
|
||||
|
||||
Ваш личный VPN с индивидуальным IP. Но не нужно его делать на весь экран по высоте, достаточно 2/3, чтобы далее контент просматривался ниже.
|
||||
|
||||
|
||||
### Преимущества
|
||||
|
||||
C иконками и расшифровкой, что это значит. Нужно написать красиво, то что перечислил ниже, можно разложить даже как-то иначе. Художественно, чтобы понятно было потребителю не разбирающемуся в технологиях, и руководителям.
|
||||
|
||||
|
||||
1. Индивидуальный ip
|
||||
- Работа с западными AI снижается риск блокировки по сравнению с публичными VPN
|
||||
- Безопасность вашего трафика в любой точке мира где вы будете пользоваться
|
||||
- Одинаковый IP на всех ваших устройствах где-бы вы не находились, не отличить. Вы всегда в одной локации с вашим IP.
|
||||
- Любые сервисы работают стабильнее с личными IP, тк при использовании массовых часто срабатывает защита и ботов
|
||||
|
||||
2. Тариф включает до 4-х устройств
|
||||
- Можно приобретать дополнительные тарифы в этой или других локациях с дополнительными индивидуальными IP
|
||||
|
||||
3. К выбранному тарифу дополнительно выделенный RU IP адрес
|
||||
- Используется и как прямое соединение и для организации моста в обе стороны
|
||||
|
||||
4. Два типа протоколов WireGuard (Amnezia WG) и Xray (Vless + Histeria2 + xhttp)
|
||||
- Дополняются и обновляются
|
||||
|
||||
5. Умная маршрутиризация трафика из Ру к Глобал
|
||||
- К RU сайтам через RU сервер напрямую
|
||||
- К Глобальным трафик через туннель прокидывается через локацию в вашем тарифе
|
||||
|
||||
6. При использовании из Глобала обратная маршрутиризация
|
||||
- К мировым сайтам прямой коннект из вашей локации
|
||||
- К RU сайтам трафик через туннель идёт через RU сервер
|
||||
- Для работающих с Россией
|
||||
|
||||
7. О нас
|
||||
- Работаем с 2022 года
|
||||
|
||||
|
||||
### FAQ
|
||||
|
||||
Стандартная история, несколько вопросов с ответами раскрывающимися. Придумай для моей тематике исходя из описанного в задаче.
|
||||
|
||||
|
||||
|
||||
## Страницы Цены
|
||||
|
||||
Сразу крупно обыграть блок с ценой на первом экране, одна цена
|
||||
|
||||
4900 - первый месяц
|
||||
2900 далее
|
||||
|
||||
Должен быть выбор локации, и при этом какие-то локации показывают, есть несколько серверов в наличии, а какие-то показывают будет готово через 24 часа.
|
||||
|
||||
Готово через 24 часа это даже фича, что покупатель реально видит что под него делается.
|
||||
|
||||
Выбор локации и Купить. Переход на страницу оформления заказа.
|
||||
|
||||
Кнопка собирать лиды, кому требуется индивидуальные условия - оставьте заявку.
|
||||
|
||||
|
||||
|
||||
|
||||
## Страница Поддержка
|
||||
|
||||
Крупно блок с контактный email и tg, дальше в FAQ разделённый по разделам и ниже форма связи.
|
||||
|
||||
|
||||
|
||||
|
||||
## Страница Оплаты и Оформления заказа
|
||||
|
||||
Страница оплаты заказа при нажатии на Купить.
|
||||
|
||||
Стандартно как у Nord и Blanc, с предложением Авторизоваться по почте.
|
||||
|
||||
|
||||
|
||||
|
||||
## Личный кабинет на Вебе
|
||||
|
||||
Максимально простой, как у Nord и Blanc. Страница и слева сайдбар. Для личного tailwind если используют, то на нём, если что-то другое применяют, то предлагай.
|
||||
|
||||
Регистрация и авторизация только по почте по одноразовому паролю, при этом не разделять авторизацию и регистрацию.
|
||||
|
||||
В сайбаре Главная, Купить Ключ, Список ключей и внизу Поддержка, Язык и Тема.
|
||||
|
||||
C Иконками пункты меню.
|
||||
|
||||
В отличии от Nord и Blanc у нас ключей может быть несколько, чтобы сразу в нужный попадать и видеть срок истечения.
|
||||
|
||||
### Главная страница
|
||||
- Список ключей
|
||||
- Купить Ключ
|
||||
- FAQ
|
||||
|
||||
### Страница Ключа
|
||||
|
||||
- Дата окончания
|
||||
- Изменить Оплату (привязанную карточку)
|
||||
- Скачать 4 конфигурации для WG или отсканировать QR
|
||||
- И ключ для Happ, crypto. Копировать и плюс QR.
|
||||
|
||||
### Страница Купить
|
||||
|
||||
Внутри личного кабинета без перехода на сайт, весь функционал как в разделе Купить с выбором локации
|
||||
|
||||
|
||||
|
||||
## Личный кабинет в Telegram
|
||||
|
||||
Нужен дизайн для MiniApp, и чтобы это выглядело в стиле телефона, например у самого Telegram, есть нативное приложение Wallet, и там похоже на интерфейс телефона.
|
||||
|
||||
Обыграть всё что я написал про Личный на Вебе, в парадигме MiniApp и интерфейса как у телефона.
|
||||
|
||||
|
||||
|
||||
## Дополнительный вариант Дизайна
|
||||
|
||||
Будет 2 одинаковых сайта, но на 1 будет продаваться только индивидуальные IP, на втором чтобы была возможно и Массовые тарифы купить и также индивидуальный. Всё одинаково, за исключением 2х тарифов, Базовый и Премиум. Везде нужно это обыграть, где представлены тарифы.
|
||||
|
||||
Но нужно максимально унифицированно.
|
||||
|
||||
Чтобы в итоге это обрабатывали одни скрипты под разными доменами будет разное видно. В тч в Личном кабинете.
|
||||
@@ -0,0 +1,77 @@
|
||||
# 00. Карта документации
|
||||
|
||||
## 1. Документы
|
||||
|
||||
| Файл | О чём |
|
||||
|---|---|
|
||||
| [01-product.md](01-product.md) | Бренды, домены, продукты, тарифы, бизнес-правила |
|
||||
| [02-architecture.md](02-architecture.md) | Стек, одно приложение на все домены, процессы, окружения, выкладка |
|
||||
| [03-data-model.md](03-data-model.md) | Коллекции Payload, поля, связи, статусы |
|
||||
| [04-billing.md](04-billing.md) | Счёт, заказы, платежи, продления, возвраты, ручные операции, рефералы |
|
||||
| [05-auth.md](05-auth.md) | Вход по коду, сессии, капча, Telegram, объединение аккаунтов, админка |
|
||||
| [06-i18n.md](06-i18n.md) | Языки, URL, определение языка, валютный переключатель, переводы |
|
||||
| [07-frontend.md](07-frontend.md) | Макеты, маршруты, токены, темы, производительность |
|
||||
| [08-integrations.md](08-integrations.md) | Почта, ALTCHA, MaxMind, Chatwoot, Twenty, Telegram, платёжные системы, Umami |
|
||||
| [09-infrastructure.md](09-infrastructure.md) | Сервер, nginx, systemd, PostgreSQL, DNS, бэкапы, задачи для root |
|
||||
| [10-security.md](10-security.md) | Безопасность и приватность |
|
||||
| [decisions/](decisions/) | Принятые решения: по файлу на решение |
|
||||
| [stages/](stages/) | Этапы: цель, границы, предусловия, шаги, критерии готовности |
|
||||
|
||||
Исходная постановка — [history/HUMAN_IDEA.md](history/HUMAN_IDEA.md) (копия файла пользователя). Это история, а не источник истины: всё актуальное — в этих документах.
|
||||
|
||||
## 2. Этапы
|
||||
|
||||
| Этап | Содержание | Статус |
|
||||
|---|---|---|
|
||||
| [0](stages/stage-0-docs.md) | Документация | готово |
|
||||
| [1a](stages/stage-1a-foundation.md) | Фундамент: Next + Payload, сайты, домены, языки, валюты, темы | не начат |
|
||||
| [1b](stages/stage-1b-marketing.md) | Маркетинговые страницы обоих брендов, контент, формы с ALTCHA, SEO, юр. тексты | не начат |
|
||||
| [1c](stages/stage-1c-auth.md) | Вход по коду, почта, сессии | не начат |
|
||||
| [1d](stages/stage-1d-billing.md) | Биллинг без эквайринга, оформление заказа, продления | не начат |
|
||||
| [1e](stages/stage-1e-keys.md) | Ключи и подключения, ручная выдача | не начат |
|
||||
| [1f](stages/stage-1f-cabinet.md) | Веб-кабинеты обоих брендов | не начат |
|
||||
| [1g](stages/stage-1g-referral.md) | Реферальная программа | не начат |
|
||||
| [1h](stages/stage-1h-launch.md) | Переводы, Umami и баннер cookies, PageSpeed, выкладка в prod | не начат |
|
||||
| [2](stages/stage-2-support-crm.md) | Chatwoot и Twenty CRM | не начат |
|
||||
| [3](stages/stage-3-telegram.md) | Telegram: боты, MiniApp, вход и привязка | не начат |
|
||||
| [4](stages/stage-4-payments.md) | Платёжные системы: Stripe, RUB-процессинг, крипта | не начат |
|
||||
| [5](stages/stage-5-keys-automation.md) | Автоматизация выдачи ключей | обсуждение при старте |
|
||||
|
||||
## 3. Как запускать этап в новой сессии
|
||||
|
||||
1. Открыть сессию Claude под пользователем vx_dev в `/home/vx_dev/veilix_dev`.
|
||||
2. Написать: «Этап 1a. Прочитай CLAUDE.md и docs/stages/stage-1a-foundation.md, сверим план».
|
||||
3. Claude сверяет план и открытые вопросы этапа, вы подтверждаете, дальше — работа.
|
||||
4. В конце Claude обновляет статус этапа в этой таблице и документацию.
|
||||
|
||||
## 4. Открытые вопросы
|
||||
|
||||
### 4.1 Решённые
|
||||
|
||||
| № | Вопрос | Решение | Где отражено |
|
||||
|---|---|---|---|
|
||||
| Q-01 | Вторая валюта | USD | [D-010](decisions/D-010-currencies.md) |
|
||||
| Q-03 | Валюта по умолчанию для нового посетителя | По языку: ru → RUB, остальные → USD; настраивается у домена | [06-i18n.md](06-i18n.md), раздел 6 |
|
||||
| Q-04 | Валюта аккаунта | Задаётся при регистрации из выбранной; меняется, пока баланс нулевой и нет ключей; дальше — через поддержку | [04-billing.md](04-billing.md), раздел 2 |
|
||||
| Q-05 | WG-конфиги mass на этапе 1 | Конфиги, добавленные администратором, по странам; другая страна — через поддержку; автоматически — этап 5 | [D-020](decisions/D-020-key-models.md) |
|
||||
| Q-06 | Где Umami | На этом сервере, без Docker, под пользователем `umami`; панель — `umi.veilix.cc` | [D-018](decisions/D-018-analytics.md) |
|
||||
| Q-07 | Как запускать сервисы | Каждый под своим пользователем ОС: vx_dev (dev), vx_prod (prod), umami; systemd user-юниты | [D-004](decisions/D-004-environments.md) |
|
||||
| Q-15 | Защита prod от сессий Claude | Отдельно не нужна: сессии Claude работают под vx_dev без доступа к prod, а под vx_prod нет ни редактора, ни ИИ-агентов | [D-004](decisions/D-004-environments.md) |
|
||||
|
||||
### 4.2 Открытые
|
||||
|
||||
Предложение по умолчанию действует, пока не решено иначе.
|
||||
|
||||
| № | Вопрос | Предложение | Нужно к этапу |
|
||||
|---|---|---|---|
|
||||
| Q-02 | Цены mass в USD, суммы пополнения и реферального бонуса в USD для обоих брендов | — нужны цифры | 1b |
|
||||
| Q-08 | Email поддержки брендов | `help@veilix.app` и `help@veilixvpn.com` (в макете были домены .cc) | 1b |
|
||||
| Q-09 | Часовой пояс часов поддержки (10–20 в рабочие дни) | МСК | 1b |
|
||||
| Q-10 | Имена Telegram-ботов: 2 основных, 2 поддержки, тестовые | — | 3 |
|
||||
| Q-11 | Оплата в Telegram: Stars, внешняя страница или только с баланса | — | 3 |
|
||||
| Q-12 | RUB-процессинг (KZ, TR) и криптопроцессинг: какие провайдеры | — | 4 |
|
||||
| Q-13 | Юрлицо для юридических текстов | Пока заглушки | 1h |
|
||||
| Q-14 | Куда складывать бэкапы prod-базы | — | 1h |
|
||||
| Q-16 | Где принимать почту поддержки (`help@…`): свой почтовый хостинг или пересылка в Chatwoot | — | 1b |
|
||||
| Q-17 | Нужны ли разные аккаунты одного провайдера для разных брендов | Пока нет. Если понадобятся — платёжной системе добавим поле «только для сайтов» | 4 |
|
||||
| Q-18 | Кто запускает выкладку в prod | Пользователь одной командой; по желанию — sudo-правило, разрешающее vx_dev запускать только команду выкладки от имени vx_prod | 1h |
|
||||
@@ -0,0 +1,155 @@
|
||||
# 01. Продукт
|
||||
|
||||
## 1. Бренды
|
||||
|
||||
| | Вейликс | ВейликсВПН |
|
||||
|---|---|---|
|
||||
| Код сайта | `dedicated` | `mass` |
|
||||
| Название | RU «Вейликс», EN «Veilix» | RU «ВейликсВПН», EN «VeilixVPN» |
|
||||
| Цвет | фиолетовый | зелёный (бирюзовый) |
|
||||
| Суть | личный VPN: у каждого ключа свой индивидуальный IP в выбранной стране | классический VPN: общий сервер, ключ даёт все страны тарифа |
|
||||
| Протоколы | WireGuard / Amnezia WG и Xray (VLESS, Hysteria2, XHTTP) | Xray и WireGuard |
|
||||
|
||||
1. Это два разных продукта на одном бэкенде и одной базе. Сайт определяется по домену.
|
||||
2. veilixvpn.com продаёт только mass. Перекрёстные ссылки между брендами допустимы: на mass есть блок «нужен свой IP — тарифы Вейликс».
|
||||
3. Логотип — вариант «Слои» из макета (два перекрывающихся скруглённых квадрата). Подборщик вариантов знака из макета в продукт не переносим.
|
||||
|
||||
## 2. Домены
|
||||
|
||||
| Назначение | Вейликс prod | Вейликс dev | ВейликсВПН prod | ВейликсВПН dev |
|
||||
|---|---|---|---|---|
|
||||
| Сайт | veilix.app | dev.veilix.app | veilixvpn.com | dev.veilixvpn.com |
|
||||
| Кабинет и оформление заказа | my.veilix.app | dev.my.veilix.app | my.veilixvpn.com | dev.my.veilixvpn.com |
|
||||
| MiniApp | tg.veilix.app | dev.tg.veilix.app | tg.veilixvpn.com | dev.tg.veilixvpn.com |
|
||||
|
||||
1. Админка одна на оба бренда: `plc.veilix.cc` (prod) и `dev.plc.veilix.cc` (dev). Аналитика Umami — `umi.veilix.cc`.
|
||||
2. Все домены указывают на 169.58.76.183 (этот сервер).
|
||||
3. Домены хранятся в базе (коллекция «Домены»), у каждого свои языки и валюты. Новое зеркало добавляется записью в базе плюс блоком nginx и сертификатом.
|
||||
|
||||
## 3. Состав каждого бренда
|
||||
|
||||
1. Сайт: главная, цены, локации, поддержка, юридические страницы.
|
||||
2. Веб-кабинет, в том числе оформление заказа (оформление живёт на my.*, а не на сайте).
|
||||
3. MiniApp в Telegram — этап 3.
|
||||
4. Два Telegram-бота: основной (команды, MiniApp, уведомления) и бот поддержки, подключённый к Chatwoot. Этапы 2–3.
|
||||
|
||||
## 4. Термины
|
||||
|
||||
1. **Сайт** — бренд (тенант). Почти все сущности привязаны к сайту.
|
||||
2. **Домен** — хост, на котором открывается сайт, кабинет или MiniApp. У домена свои языки и валюты.
|
||||
3. **Тариф** — то, что продаётся: цены в валютах, период, лимит устройств, страны.
|
||||
4. **Страна** — общий справочник. **Локация** — страна на конкретном сайте: город, статус наличия.
|
||||
5. **Ключ** — купленная единица доступа пользователя со сроком действия и автопродлением.
|
||||
6. **Подключение** — конкретная конфигурация внутри ключа: WG или AWG-конфиг устройства, ссылка Happ, ссылка Xray.
|
||||
7. **Заказ, Платёж, Движение по счёту** — см. [04-billing.md](04-billing.md).
|
||||
8. **Валюта** и **Платёжная система** — справочники; платёжная система доступна в определённых валютах.
|
||||
|
||||
## 5. Dedicated (Вейликс)
|
||||
|
||||
1. Ключ — это индивидуальный IP в выбранной стране. Страна выбирается при покупке и не меняется никогда. Нужна другая страна — пользователь покупает новый ключ в другой локации и отключает автопродление старого. Отдельного функционала смены страны нет.
|
||||
2. В каждый ключ входит выделенный RU-адрес: работает и как прямое соединение, и как мост в обе стороны.
|
||||
3. До 5 устройств на ключ (поле тарифа), все с одним IP. WireGuard — по конфигу на устройство. Xray — ссылка для Happ (обычная или зашифрованная crypt-ссылка Happ, которая скрывает адрес подписки).
|
||||
4. Тариф — ценовой уровень со своим набором стран:
|
||||
|
||||
| Тариф | Страны | Первый месяц | Далее в месяц | USD (из макета) |
|
||||
|---|---|---|---|---|
|
||||
| Базовый | 23 страны (см. раздел 7) | 4 900 ₽ | 2 499 ₽ | $49 / $24 |
|
||||
| Редкие локации | CH, AE, SG, JP, KR | 6 900 ₽ | 3 499 ₽ | $69 / $34 |
|
||||
| Дальние локации | AU, BR | 9 900 ₽ | 4 999 ₽ | $99 / $49 |
|
||||
|
||||
5. Первый месяц дороже, потому что включает подготовку сервера: выделение адреса, настройку протоколов и маршрутизации. Период — месяц.
|
||||
6. Статусы локаций: «Серверы в наличии» и «Установим за 24 часа». Оплаченный период начинается с момента выдачи ключа. На этапе 1 ключи выдаёт администратор вручную, поэтому так работает для обоих статусов.
|
||||
7. Каждый ключ продлевается независимо, в свою дату.
|
||||
|
||||
## 6. Mass (ВейликсВПН)
|
||||
|
||||
1. Ключ включает все страны тарифа. Страна при покупке не выбирается: список стран на сайте только показывает, что входит в ключ.
|
||||
2. Happ — одна ссылка со всеми странами. WireGuard — пользователь может получить конфиги любой страны тарифа. На этапе 1 это конфиги, которые добавил администратор, по странам; нужна другая страна — через поддержку. Автоматически — на этапе 5.
|
||||
3. Тарифы: «Месяц» 699 ₽ и «Год» 6 990 ₽ («582 ₽ в месяц, экономия 1 398 ₽»). Платы за подключение и доплат за страну нет: первый период стоит столько же, сколько продление.
|
||||
4. До 5 устройств на ключ (поле тарифа).
|
||||
5. Статусы локаций: «Включено» и «Скоро». Локации «Скоро» показываются, но не работают.
|
||||
6. В кабинете — «Список ключей», как у dedicated; внутри ключа ссылка Xray и конфиги WG. Формулировки «текущая/запасная локация» из макета не используем.
|
||||
7. Цены в USD для mass в макете нет — вопрос Q-02.
|
||||
|
||||
## 7. Локации из макета
|
||||
|
||||
Город, статус и ценовой уровень dedicated. На mass те же 30 стран: «готова» = «Включено», «24 часа» = «Скоро».
|
||||
|
||||
| Код | Страна | Город | Статус | Уровень |
|
||||
|---|---|---|---|---|
|
||||
| NL | Нидерланды | Амстердам | готова | базовый |
|
||||
| DE | Германия | Франкфурт | готова | базовый |
|
||||
| US | США | Нью-Йорк | готова | базовый |
|
||||
| GB | Великобритания | Лондон | готова | базовый |
|
||||
| FR | Франция | Париж | готова | базовый |
|
||||
| FI | Финляндия | Хельсинки | готова | базовый |
|
||||
| SE | Швеция | Стокгольм | готова | базовый |
|
||||
| NO | Норвегия | Осло | 24 часа | базовый |
|
||||
| DK | Дания | Копенгаген | 24 часа | базовый |
|
||||
| CH | Швейцария | Цюрих | готова | редкие |
|
||||
| AT | Австрия | Вена | готова | базовый |
|
||||
| ES | Испания | Мадрид | готова | базовый |
|
||||
| IT | Италия | Милан | готова | базовый |
|
||||
| PL | Польша | Варшава | готова | базовый |
|
||||
| CZ | Чехия | Прага | готова | базовый |
|
||||
| RO | Румыния | Бухарест | 24 часа | базовый |
|
||||
| LT | Литва | Вильнюс | готова | базовый |
|
||||
| LV | Латвия | Рига | готова | базовый |
|
||||
| EE | Эстония | Таллин | 24 часа | базовый |
|
||||
| TR | Турция | Стамбул | готова | базовый |
|
||||
| KZ | Казахстан | Алматы | 24 часа | базовый |
|
||||
| AM | Армения | Ереван | 24 часа | базовый |
|
||||
| GE | Грузия | Тбилиси | 24 часа | базовый |
|
||||
| AE | ОАЭ | Дубай | готова | редкие |
|
||||
| SG | Сингапур | Сингапур | готова | редкие |
|
||||
| JP | Япония | Токио | готова | редкие |
|
||||
| KR | Южная Корея | Сеул | 24 часа | редкие |
|
||||
| CA | Канада | Торонто | готова | базовый |
|
||||
| AU | Австралия | Сидней | 24 часа | дальние |
|
||||
| BR | Бразилия | Сан-Паулу | 24 часа | дальние |
|
||||
|
||||
Координаты для карты — в массиве `L(...)` в `design/handoff/project/Price.dc.html`. Количество свободных серверов из MiniApp (NL 12, DE 10, …) — демо-данные. Показывать ли счётчик на сайте — решается в этапе 1b.
|
||||
|
||||
## 8. Пользователи
|
||||
|
||||
1. Аккаунты раздельные по сайтам: один email на veilix.app и на veilixvpn.com — два разных пользователя.
|
||||
2. Регистрация и вход — одно поле email и одноразовый код. Отдельной регистрации нет.
|
||||
3. Telegram (этап 3): аккаунт можно создать по Telegram без почты. Почта привязывается кодом, после этого работает вход на сайте. На сайте есть кнопка «Войти через Telegram». Правила объединения аккаунтов в пределах сайта — [05-auth.md](05-auth.md).
|
||||
|
||||
## 9. Деньги
|
||||
|
||||
1. У пользователя есть счёт (баланс) в валюте аккаунта.
|
||||
2. Валюты — отдельный справочник: RUB и USD. Переключатель валюты не зависит от языка: из России часто заходят через VPN, поэтому страна по IP для выбора валюты не годится. По умолчанию валюта зависит от языка: ru → RUB, остальные → USD.
|
||||
3. Платёжные системы привязаны только к валютам. Где система доступна, следует из цепочки: валюты → домены, на которых они включены → сайты:
|
||||
a. RUB — процессинги, принимающие карты РФ (вероятно, из Казахстана и Турции), без автосписаний. ЮKassa — опционально, только для пополнений;
|
||||
b. USD — Stripe (карты, сохранение карты, автосписания) и криптовалюта.
|
||||
4. Карта привязывается к аккаунту, а не к ключу (Stripe). Отвязать карту можно в аккаунте.
|
||||
5. Продление: у каждого ключа галочка автопродления и кнопка «Продлить сейчас». Сначала списывается баланс, нехватка — с карты.
|
||||
6. Не хватило денег: льготный период 3 дня (ключ работает), затем ключ истекает. Напоминание за 3 дня до окончания.
|
||||
7. Ручные операции в админке: платёж через платёжную систему manual (проводится как настоящий), корректировка баланса, возврат на баланс.
|
||||
8. Возвраты (политика из макета): если сервер не заработал по нашей вине — полностью в течение первых 7 дней; если пользователь передумал — остаток неиспользованного периода.
|
||||
9. Подробно — [04-billing.md](04-billing.md).
|
||||
|
||||
## 10. Реферальная программа
|
||||
|
||||
1. «Приведите друга — вы оба получите по месяцу в подарок».
|
||||
2. «Месяц» начисляется деньгами на баланс обоим после первой оплаты заказа другом. Сумма задаётся в админке для каждого сайта и валюты; в макете — 2 499 ₽ (dedicated) и 699 ₽ (mass).
|
||||
3. Бонус попадает на баланс и поэтому уходит в ближайшее списание.
|
||||
4. Ссылки: `https://my.veilix.app/r/<код>` (для mass — `my.veilixvpn.com`) и `t.me/<основной бот>?start=<код>`.
|
||||
5. Приглашения между брендами не засчитываются, самоприглашения отсекаются.
|
||||
6. В кабинете показываем: приглашено, начислено, ожидает оплаты; список приглашённых с замаскированной почтой.
|
||||
|
||||
## 11. Поддержка и заявки
|
||||
|
||||
1. Страница поддержки: Telegram (бот поддержки бренда) и email, FAQ по разделам, форма связи.
|
||||
2. Заявка «Нужны индивидуальные условия» (dedicated) и «Нужны особые условия» (mass): имя, email, Telegram, WhatsApp, коротко о задаче.
|
||||
3. Заявки и обращения сохраняются в Payload и приходят на почту поддержки (этап 1), затем уходят в Twenty CRM и Chatwoot (этап 2).
|
||||
4. Часы поддержки из макета: с 10 до 20 в рабочие дни (часовой пояс — Q-09).
|
||||
|
||||
## 12. Языки
|
||||
|
||||
en (по умолчанию, без префикса в URL), ru, de, fr, es. Позже — pt-br и ja. Подробно — [06-i18n.md](06-i18n.md).
|
||||
|
||||
## 13. Юридические страницы
|
||||
|
||||
Условия использования, политика конфиденциальности, политика возвратов, политика cookies. Черновики на RU и EN готовятся в этапе 1b, с заглушками вместо данных юрлица (Q-13). Это черновики, не юридическая консультация: перед запуском — показать юристу. Stripe при подключении проверяет эти страницы.
|
||||
@@ -0,0 +1,116 @@
|
||||
# 02. Архитектура
|
||||
|
||||
## 1. Стек
|
||||
|
||||
| Что | Версия на 20.10.2026 | Комментарий |
|
||||
|---|---|---|
|
||||
| Node.js | 24 LTS (на сервере 24.22) | |
|
||||
| pnpm | 12.x | ставится root через `corepack enable pnpm`; версия фиксируется полем `packageManager` |
|
||||
| Next.js | 16.4.x | App Router, React Server Components |
|
||||
| React | 19.x | |
|
||||
| Payload CMS | 3.91.x | Payload 4 в бете; переход после стабильного релиза ([D-002](decisions/D-002-payload-version.md)) |
|
||||
| PostgreSQL | 18 (на хосте) | адаптер `@payloadcms/db-postgres`, `idType: 'uuid'` |
|
||||
| Tailwind CSS | 4.x | токены из макета как CSS-переменные |
|
||||
| next-intl | 4.x | переводы интерфейса, префиксы языков в URL |
|
||||
| Nodemailer | через `@payloadcms/email-nodemailer` | транспорты SMTP, sendmail, «только журнал» |
|
||||
| ALTCHA | 3.x | капча без третьих сторон |
|
||||
| grammY | 1.x | Telegram-боты, этап 3 |
|
||||
| Stripe SDK | актуальный | этап 4 |
|
||||
|
||||
Точные версии фиксируются в `package.json` на этапе 1a. Если к старту этапа 1a выйдет стабильный Payload 4, начинаем сразу на нём.
|
||||
|
||||
## 2. Одно приложение на все домены
|
||||
|
||||
1. Одно Next.js-приложение, Payload установлен внутрь как библиотека: админка, API и фронтенды — один процесс ([D-001](decisions/D-001-single-app.md)).
|
||||
2. Proxy Next.js (`src/proxy.ts`, бывший middleware) по заголовку Host находит запись в коллекции «Домены» и узнаёт сайт (`dedicated` или `mass`) и тип фронтенда: сайт, кабинет или MiniApp. Список доменов кэшируется в памяти и сбрасывается при изменении записи.
|
||||
3. Proxy переписывает запрос во внутренний маршрут вида `/{тип}/{сайт}/{язык}/…`, для маркетинговых страниц ещё и `/{валюта}/`. Снаружи эти пути недоступны: прямой запрос к ним даёт 404.
|
||||
4. Хост админки (`plc.veilix.cc`, `dev.plc.veilix.cc`) задаётся в `.env`. Только на нём работают `/admin` и REST API Payload. На доменах брендов эти пути закрыты.
|
||||
5. Сайтам, кабинетам и MiniApp хост админки не нужен:
|
||||
a. данные Payload они читают в том же процессе через Local API — это вызов функции, а не HTTP-запрос;
|
||||
b. запросы браузера (формы, вход, действия в кабинете) идут на тот домен, где открыта страница: server actions и эндпоинты `/svc/…`;
|
||||
c. вебхуки платёжных систем и Telegram приходят на публичные домены, не на хост админки;
|
||||
d. файлы медиатеки отдаются через домены сайтов.
|
||||
6. Ядро общее (биллинг, ключи, вход, рефералы, почта), UI-кит общий, а страницы у каждого бренда свои: данные в дизайнах представлены по-разному.
|
||||
|
||||
## 3. Структура кода (план)
|
||||
|
||||
```
|
||||
src/
|
||||
proxy.ts — домен → сайт, тип фронтенда, язык, валюта
|
||||
payload.config.ts
|
||||
app/
|
||||
(payload)/ — админка и REST API Payload (стандартные файлы)
|
||||
(front)/m/[site]/[locale]/[currency]/… — маркетинговые сайты
|
||||
(front)/c/[site]/[locale]/… — кабинеты и оформление заказа
|
||||
(front)/t/[site]/… — MiniApp (этап 3)
|
||||
svc/… — свои эндпоинты: ALTCHA, вебхуки, health
|
||||
payload/
|
||||
collections/ globals/ hooks/ access/ fields/
|
||||
core/ — бизнес-логика без привязки к UI
|
||||
billing/ keys/ auth/ referral/ email/ payments/ currency/ geo/
|
||||
brands/
|
||||
dedicated/ mass/ — страницы и компоненты каждого бренда
|
||||
ui/ — общие компоненты: кнопки, поля, шиты, переключатели, флаги, карта
|
||||
i18n/ — конфиг next-intl и messages/{en,ru,de,fr,es}.json
|
||||
styles/ — Tailwind, токены брендов, темы
|
||||
```
|
||||
|
||||
Бизнес-логика живёт в `core/` и вызывается одинаково из хуков Payload, server actions, вебхуков, фоновых задач и (на этапе 3) бота.
|
||||
|
||||
## 4. Окружения
|
||||
|
||||
| | dev | prod |
|
||||
|---|---|---|
|
||||
| Пользователь ОС | vx_dev (под ним работают редактор и сессии Claude) | vx_prod (без редактора и ИИ-агентов) |
|
||||
| Папка | `/home/vx_dev/veilix_dev` — рабочая копия git | `/home/vx_prod/veilix` — `releases/`, симлинк `current`, `shared/` |
|
||||
| Запуск | `pnpm dev` (горячая перезагрузка) | `pnpm start` из `current` после `pnpm build` |
|
||||
| Порт | 3100 | 3000 |
|
||||
| База | `veilix_dev` | `veilix_prod` |
|
||||
| Схема базы | синхронизируется автоматически (push) | миграции Payload, применяются при выкладке |
|
||||
| Домены | `dev.*`, `dev.plc.veilix.cc` | основные, `plc.veilix.cc` |
|
||||
| Секреты | `/home/vx_dev/veilix_dev/.env` | `/home/vx_prod/veilix/shared/.env` |
|
||||
| Файлы (media) | `/home/vx_dev/veilix_dev/media` | `/home/vx_prod/veilix/shared/media` |
|
||||
|
||||
1. Без Docker: оба окружения — обычные процессы Node, каждый под своим пользователем ОС, запускаются одинаково — systemd user-юнитами: `veilix-dev.service` у vx_dev, `veilix.service` у vx_prod. У vx_dev нет доступа к файлам, секретам, логам и базе prod. Подробно — [D-003](decisions/D-003-hosting.md), [D-004](decisions/D-004-environments.md).
|
||||
2. Dev закрыт от посторонних в nginx и запрещён к индексации, см. [09-infrastructure.md](09-infrastructure.md).
|
||||
3. Платёжные системы в dev — только в тестовом режиме.
|
||||
|
||||
## 5. Выкладка в prod
|
||||
|
||||
1. В dev: изменения схемы оформляются миграцией (`payload migrate:create`), коммит, push в Gitea, тег релиза.
|
||||
2. Выкладка — команда `deploy <тег>` пользователя vx_prod, появится на этапе 1h. Запускает её пользователь; по желанию — sudo-правило, разрешающее vx_dev запускать только эту команду (Q-18). Шаги:
|
||||
a. взять тег из Gitea (у vx_prod ключ только на чтение) в новую папку `releases/<дата-тег>`;
|
||||
b. `pnpm install --frozen-lockfile` и `pnpm build`; ссылки на `shared/.env` и `shared/media`;
|
||||
c. применить миграции к `veilix_prod`;
|
||||
d. переключить симлинк `current` на новый релиз и перезапустить `veilix.service`;
|
||||
e. проверить `/svc/health`; хранить 5 последних релизов.
|
||||
3. Откат: переключить `current` на прошлый релиз и перезапустить. Миграции пишем так, чтобы прошлый релиз продолжал работать с новой схемой (сначала добавляем, удаляем позже).
|
||||
4. Простой при выкладке — только время перезапуска процесса, несколько секунд.
|
||||
|
||||
## 6. Рендеринг и кэширование
|
||||
|
||||
1. Маркетинговые страницы собираются статически во всех вариантах «язык × валюта» и кэшируются. Proxy выбирает вариант по префиксу языка и cookie валюты.
|
||||
2. Изменение в админке (цена, статус локации, FAQ) вызывает хук Payload, и Next.js пересобирает затронутые страницы за секунды (on-demand revalidation по тегам). Запасной вариант — плановое обновление раз в 5 минут.
|
||||
3. При оформлении заказа наличие и цена проверяются заново на сервере. Устаревшая страница не может продать то, чего нет.
|
||||
4. Кабинет рендерится на сервере (SSR) для каждого запроса, без кэша.
|
||||
5. Маркетинговые домены не ставят cookies, кроме выбора языка, валюты и темы.
|
||||
|
||||
## 7. Фоновые задачи
|
||||
|
||||
Встроенная очередь Payload (Jobs Queue) с расписанием cron. Исполнитель работает внутри процесса приложения (`autoRun`), в dev его можно отключить переменной окружения.
|
||||
|
||||
| Задача | Расписание | Что делает |
|
||||
|---|---|---|
|
||||
| `renewals` | каждые 15 минут | продление ключей, у которых срок подходит (см. [04-billing.md](04-billing.md)) |
|
||||
| `reminders` | каждый час | напоминания за 3 дня до окончания |
|
||||
| `expiry` | каждые 15 минут | перевод в льготный период и в «истёк» |
|
||||
| `send-email` | по событию | отправка письма с повторами при ошибке |
|
||||
| `cleanup` | раз в сутки | старые коды входа, сессии, записи журнала писем |
|
||||
| `reconcile` | раз в сутки | сверка кэша баланса с суммой движений по счёту |
|
||||
|
||||
## 8. Чего не используем
|
||||
|
||||
1. Docker — не нужен ни в dev, ни в prod ([D-003](decisions/D-003-hosting.md)).
|
||||
2. n8n — боты пишем в коде на grammY ([D-015](decisions/D-015-telegram-bots.md)).
|
||||
3. listmonk — шаблоны писем в Payload, отправка своим транспортом ([D-014](decisions/D-014-email.md)).
|
||||
4. Redis — стоит на сервере, но пока не нужен: лимиты запросов и очередь задач работают на PostgreSQL.
|
||||
@@ -0,0 +1,374 @@
|
||||
# 03. Модель данных
|
||||
|
||||
## 1. Соглашения
|
||||
|
||||
1. Все `id` — UUID (`idType: 'uuid'` в адаптере Postgres). Номера для людей (заказы, ключи, платежи, реферальные коды) — случайные коды, не последовательные.
|
||||
2. Сайт — тенант плагина `@payloadcms/plugin-multi-tenant`. Поле тенанта называется `site`. Коллекции с пометкой «сайт» содержат его автоматически; в админке у них есть переключатель сайта.
|
||||
3. «(L)» — локализованное поле Payload: значение хранится на каждом языке, если перевода нет — показывается EN.
|
||||
4. Деньги — целое число в минимальных единицах (`amount`: 249900 = 2 499,00 ₽) плюс `currency` — код ISO 4217 (`RUB`, `USD`). Код проверяется по справочнику «Валюты».
|
||||
5. Даты — `timestamptz`, в UTC.
|
||||
6. Секреты в полях (конфиги WG, ссылки подписок, SMTP-пароль, id сохранённых карт) шифруются приложением (AES-256-GCM, ключ в `.env`).
|
||||
|
||||
## 2. Схема связей
|
||||
|
||||
```mermaid
|
||||
erDiagram
|
||||
SITES ||--o{ DOMAINS : "домены"
|
||||
SITES ||--o{ LOCATIONS : "локации"
|
||||
SITES ||--o{ TARIFFS : "тарифы"
|
||||
SITES ||--o{ CUSTOMERS : "пользователи"
|
||||
COUNTRIES ||--o{ LOCATIONS : "страна"
|
||||
TARIFFS }o--o{ COUNTRIES : "страны тарифа"
|
||||
TARIFFS }o--o{ TAGS : "теги"
|
||||
DOMAINS }o--o{ CURRENCIES : "валюты домена"
|
||||
PAYMENT_SYSTEMS }o--o{ CURRENCIES : "валюты системы"
|
||||
CUSTOMERS ||--o{ KEYS : "ключи"
|
||||
TARIFFS ||--o{ KEYS : "тариф"
|
||||
LOCATIONS ||--o{ KEYS : "локация (dedicated)"
|
||||
KEYS ||--o{ CONNECTIONS : "подключения"
|
||||
CUSTOMERS ||--o{ ORDERS : "заказы"
|
||||
KEYS ||--o{ ORDERS : "продления"
|
||||
ORDERS ||--o{ PAYMENTS : "оплаты"
|
||||
CUSTOMERS ||--o{ PAYMENTS : "платежи"
|
||||
PAYMENT_SYSTEMS ||--o{ PAYMENTS : "через"
|
||||
CUSTOMERS ||--o{ LEDGER_ENTRIES : "движения"
|
||||
PAYMENTS ||--o{ LEDGER_ENTRIES : "зачисление"
|
||||
ORDERS ||--o{ LEDGER_ENTRIES : "списание, возврат"
|
||||
CUSTOMERS ||--o{ PAYMENT_METHODS : "карты"
|
||||
CUSTOMERS ||--o{ REFERRALS : "пригласил"
|
||||
CUSTOMERS ||--o{ CUSTOMER_SESSIONS : "сессии"
|
||||
```
|
||||
|
||||
## 3. Настройки и справочники
|
||||
|
||||
### 3.1 `sites` — Сайты (тенанты)
|
||||
|
||||
| Поле | Тип | Описание |
|
||||
|---|---|---|
|
||||
| code | select `dedicated` \| `mass`, уникальное | бренд: по нему выбираются тема и страницы |
|
||||
| name | text (L) | «Вейликс» / «Veilix» |
|
||||
| orderPrefix | text | префикс номеров: `VX`, `VV` |
|
||||
| supportEmail | email | почта поддержки (Q-08) |
|
||||
| emailFrom | group: name, address | отправитель писем, напр. «Вейликс» <no-reply@veilix.app> |
|
||||
| supportBot, mainBot | text | имена ботов (этап 3) |
|
||||
| referralEnabled | checkbox | |
|
||||
| referralBonus | array: currency, amount | бонус «месяц в подарок» в каждой валюте |
|
||||
| topupPresets | array: currency, amounts[] | суммы пополнения в кабинете |
|
||||
| graceDays | number, по умолчанию 3 | льготный период |
|
||||
| reminderDays | number, по умолчанию 3 | за сколько дней напоминать |
|
||||
| renewalLeadHours | number, по умолчанию 24 | за сколько часов до окончания пробовать продлить |
|
||||
| expiringBadgeDays | number, по умолчанию 7 | когда показывать «Истекает» |
|
||||
| seo | group: titleTemplate (L), description (L), ogImage | значения по умолчанию |
|
||||
|
||||
### 3.2 `domains` — Домены (сайт)
|
||||
|
||||
| Поле | Тип | Описание |
|
||||
|---|---|---|
|
||||
| hostname | text, уникальное | `veilix.app`, `my.veilix.app`, … |
|
||||
| app | select `marketing` \| `cabinet` \| `miniapp` | тип фронтенда |
|
||||
| locales | multi-select из языков конфига | включённые языки |
|
||||
| defaultLocale | select | обычно `en` |
|
||||
| currencies | multi-select валют | включённые валюты |
|
||||
| defaultCurrency | select | валюта по умолчанию |
|
||||
| localeCurrency | array: locale, currency | валюта по умолчанию для языка: ru → RUB |
|
||||
| isPrimary | checkbox | основной домен этого типа; используется в ссылках и sitemap |
|
||||
| indexable | checkbox | разрешена ли индексация (для кабинета и MiniApp — нет) |
|
||||
| umamiWebsiteId | text | этап 1h |
|
||||
| active | checkbox | |
|
||||
|
||||
### 3.3 `currencies` — Валюты
|
||||
|
||||
| Поле | Тип | Описание |
|
||||
|---|---|---|
|
||||
| code | text, уникальное | ISO 4217: `RUB`, `USD` |
|
||||
| symbol | text | `₽`, `$` |
|
||||
| name | text (L) | |
|
||||
| minorUnits | number | 2 |
|
||||
| active, sort | | |
|
||||
|
||||
### 3.4 `payment-systems` — Платёжные системы
|
||||
|
||||
Платёжная система привязана только к валютам. Где она доступна, следует из цепочки: валюты → домены, на которых они включены → сайты. В оформлении заказа и пополнении показываются системы в валюте аккаунта.
|
||||
|
||||
| Поле | Тип | Описание |
|
||||
|---|---|---|
|
||||
| name | text | название в админке |
|
||||
| adapter | select | реализация в коде: `manual`; на этапе 4 — `stripe`, `yookassa`, `crypto_*`, `rucard_*` |
|
||||
| title | text (L) | что видит пользователь: «Банковская карта», «Карта МИР», «СБП», «Криптовалюта» |
|
||||
| description | text (L) | подсказка: «USDT, BTC, ETH, курс фиксируется на 15 минут» |
|
||||
| icon | upload | |
|
||||
| currencies | multi-select валют | в каких валютах доступна |
|
||||
| purposes | multi-select `order` \| `topup` | оплата заказа, пополнение |
|
||||
| visibleToCustomers | checkbox | у `manual` выключено: ей пользуются только администраторы |
|
||||
| mode | select `test` \| `live` | |
|
||||
| limits | array: currency, min, max | |
|
||||
| capabilities | только чтение, из адаптера | `recurring`, `refunds`, `saveMethod` |
|
||||
| active, sort | | |
|
||||
|
||||
Ключи доступа к провайдерам — только в `.env` (`PAYMENT_<КОД>_…`), не в базе.
|
||||
|
||||
### 3.5 `countries` — Страны (общие)
|
||||
|
||||
| Поле | Тип | Описание |
|
||||
|---|---|---|
|
||||
| iso2 | text, уникальное | `NL`, `DE`, … |
|
||||
| name | text (L) | |
|
||||
| lat, lng | number | точка на карте по умолчанию |
|
||||
| sort | number | |
|
||||
|
||||
### 3.6 `locations` — Локации (сайт)
|
||||
|
||||
| Поле | Тип | Описание |
|
||||
|---|---|---|
|
||||
| country | relationship → countries | уникальна в пределах сайта |
|
||||
| city | text (L) | |
|
||||
| lat, lng | number | точка города на карте |
|
||||
| status | select `available` \| `on_demand` \| `soon` | dedicated: «Серверы в наличии» / «Установим за 24 часа»; mass: «Включено» / «Скоро» |
|
||||
| freeServers | number, необязательное | счётчик (решение о показе — этап 1b) |
|
||||
| visible, sort | | |
|
||||
|
||||
### 3.7 `tags` — Теги (общие)
|
||||
|
||||
`name`, `code` (уникальное). Для фильтрации тарифов и будущей автоматизации выдачи ключей.
|
||||
|
||||
### 3.8 `tariffs` — Тарифы (сайт)
|
||||
|
||||
| Поле | Тип | Описание |
|
||||
|---|---|---|
|
||||
| name | text (L) | |
|
||||
| code | text, уникальное в сайте | |
|
||||
| type | select `dedicated` \| `mass` | поведение ключа: dedicated — страна выбирается при покупке, mass — все страны тарифа |
|
||||
| description | text (L) | |
|
||||
| features | array of text (L) | пункты «что входит» |
|
||||
| period | group: unit `month` \| `year`, count | |
|
||||
| devicesLimit | number, по умолчанию 5 | |
|
||||
| prices | array: currency, firstPeriodAmount, renewalAmount | строка на валюту; нет строки — тариф в этой валюте не продаётся |
|
||||
| countries | relationship → countries, many | страны тарифа |
|
||||
| tags | relationship → tags, many | |
|
||||
| badge | text (L) | «выгоднее» |
|
||||
| isDefault | checkbox | выбран по умолчанию на странице цен |
|
||||
| active, sort | | |
|
||||
|
||||
## 4. Люди
|
||||
|
||||
### 4.1 `admins` — Администраторы
|
||||
|
||||
Стандартная авторизация Payload (email и пароль), `maxLoginAttempts: 5`, `lockTime: 15 мин`.
|
||||
|
||||
| Поле | Тип | Описание |
|
||||
|---|---|---|
|
||||
| name | text | |
|
||||
| roles | multi-select `owner` \| `admin` \| `support` \| `content` | см. раздел 8 |
|
||||
| sites | доступ к тенантам | owner и admin видят все сайты |
|
||||
|
||||
### 4.2 `customers` — Пользователи (сайт)
|
||||
|
||||
Коллекция авторизации Payload со своей стратегией (вход по коду, [05-auth.md](05-auth.md)).
|
||||
|
||||
| Поле | Тип | Описание |
|
||||
|---|---|---|
|
||||
| email | email, null | уникален в пределах сайта, хранится в нижнем регистре |
|
||||
| emailVerifiedAt | date | |
|
||||
| telegramId | text, null | уникален в пределах сайта (этап 3) |
|
||||
| telegramUsername, telegramName | text | этап 3 |
|
||||
| locale | select | язык писем и бота |
|
||||
| currency | select | валюта аккаунта ([04-billing.md](04-billing.md), раздел 2) |
|
||||
| balance | number, только чтение | кэш суммы движений по счёту |
|
||||
| referralCode | text, уникальное в сайте | случайный |
|
||||
| referredBy | relationship → customers | |
|
||||
| status | select `active` \| `blocked` | |
|
||||
| registeredVia | select `email` \| `telegram` \| `admin` | |
|
||||
| registrationIpHash | text | HMAC от IP: только для сравнения при проверке самоприглашений |
|
||||
| marketingConsent | checkbox | |
|
||||
| lastLoginAt | date | |
|
||||
| defaultPaymentMethod | relationship → payment-methods | |
|
||||
| adminNotes | textarea | видят только администраторы |
|
||||
|
||||
### 4.3 `customer-sessions` — Сессии (служебная)
|
||||
|
||||
`customer`, `kind` (`web` \| `miniapp`), `tokenHash` (SHA-256, уникальное), `createdAt`, `expiresAt`, `lastSeenAt`, `ipHash`, `userAgent`, `revokedAt`.
|
||||
|
||||
### 4.4 `auth-codes` — Коды входа (служебная)
|
||||
|
||||
`site`, `email`, `purpose` (`login` \| `link_email`), `customer` (для привязки почты), `codeHash`, `expiresAt`, `attempts`, `consumedAt`, `requestIpHash`.
|
||||
|
||||
## 5. Ключи
|
||||
|
||||
### 5.1 `keys` — Ключи (сайт)
|
||||
|
||||
| Поле | Тип | Описание |
|
||||
|---|---|---|
|
||||
| code | text, уникальное | публичный код, напр. `K-7F2C9Q` |
|
||||
| customer | relationship | |
|
||||
| tariff | relationship | |
|
||||
| type | select `dedicated` \| `mass` | копия из тарифа |
|
||||
| location | relationship → locations | только dedicated |
|
||||
| status | select | см. 5.3 |
|
||||
| autoRenew | checkbox, по умолчанию да | |
|
||||
| activatedAt, expiresAt, graceUntil, cancelledAt | date | |
|
||||
| renewalAmount, currency | | цена продления, зафиксированная при покупке; администратор может изменить |
|
||||
| period, devicesLimit | | копии из тарифа на момент покупки |
|
||||
| ipAddress, ruIpAddress | text | для показа (dedicated) |
|
||||
| adminNotes | textarea | |
|
||||
|
||||
### 5.2 `connections` — Подключения
|
||||
|
||||
| Поле | Тип | Описание |
|
||||
|---|---|---|
|
||||
| key | relationship | |
|
||||
| kind | select `wireguard` \| `amneziawg` \| `happ` \| `xray` \| `other` | `happ` — ссылка подписки или crypt-ссылка Happ, `xray` — ссылка `vless://` и подобные |
|
||||
| title | text | «Устройство 1», «Ноутбук» |
|
||||
| deviceSlot | number | номер устройства для WG/AWG |
|
||||
| country | relationship → countries | для WG у mass — страна конфига; для RU-адреса — RU |
|
||||
| isRu | checkbox | подключение к RU-адресу (dedicated) |
|
||||
| payload | text, зашифрованное | содержимое конфига или URL |
|
||||
| fileName | text | имя файла при скачивании |
|
||||
| status | select `active` \| `revoked` | |
|
||||
| source | select `manual` \| `auto` | |
|
||||
| createdBy | relationship → admins | |
|
||||
|
||||
QR-коды строятся на лету из `payload`, не хранятся.
|
||||
|
||||
### 5.3 Статусы ключа
|
||||
|
||||
| Статус | Значение |
|
||||
|---|---|
|
||||
| `provisioning` | оплачен, готовится. Срок ещё не идёт. На этапе 1 так создаются все ключи: подключения добавляет администратор |
|
||||
| `active` | работает, срок идёт с `activatedAt` |
|
||||
| `grace` | срок истёк, оплата не прошла, ключ ещё работает до `graceUntil` |
|
||||
| `expired` | истёк, подключения отключаются |
|
||||
| `cancelled` | отменён (возврат) |
|
||||
| `suspended` | заблокирован администратором |
|
||||
|
||||
Переходы — в [04-billing.md](04-billing.md).
|
||||
|
||||
## 6. Деньги
|
||||
|
||||
### 6.1 `orders` — Заказы (сайт)
|
||||
|
||||
| Поле | Тип | Описание |
|
||||
|---|---|---|
|
||||
| number | text, уникальное | `VX-7K3M9Q` |
|
||||
| customer | relationship | |
|
||||
| type | select `new_key` \| `renewal` | |
|
||||
| tariff | relationship | |
|
||||
| location | relationship | для `new_key` у dedicated |
|
||||
| key | relationship | для `renewal`; для `new_key` заполняется после оплаты |
|
||||
| renewalFor | date | `expiresAt` ключа, который продлеваем; уникально вместе с `key` |
|
||||
| currency, amount | | полная стоимость |
|
||||
| status | select `pending` \| `paid` \| `cancelled` \| `expired` \| `refunded` \| `partially_refunded` | |
|
||||
| source | select `web` \| `miniapp` \| `bot` \| `admin` \| `auto_renewal` | |
|
||||
| paidAt, cancelledAt, validUntil | date | `validUntil` — неоплаченный заказ истекает через 24 часа |
|
||||
| refundedAmount | number | |
|
||||
| adminNotes | textarea | |
|
||||
|
||||
### 6.2 `payments` — Платежи (сайт)
|
||||
|
||||
| Поле | Тип | Описание |
|
||||
|---|---|---|
|
||||
| number | text, уникальное | |
|
||||
| customer | relationship | |
|
||||
| paymentSystem | relationship | |
|
||||
| adapter | text | копия из платёжной системы |
|
||||
| purpose | select `order` \| `topup` | |
|
||||
| order | relationship | для `order` |
|
||||
| currency, amount | | |
|
||||
| status | select `pending` \| `succeeded` \| `failed` \| `cancelled` \| `refunded` \| `partially_refunded` | |
|
||||
| providerPaymentId | text | уникально вместе с `adapter` |
|
||||
| providerStatus, providerData | text, json | только для администраторов |
|
||||
| methodSummary | group: type, brand, last4 | «Карта ···· 4242», «USDT (TRC-20)» |
|
||||
| savedPaymentMethod | relationship | если карта сохранена |
|
||||
| idempotencyKey | text, уникальное | |
|
||||
| succeededAt, processedAt, failedAt | date | `processedAt` — цепочка действий выполнена |
|
||||
| failureReason | text | |
|
||||
| createdBy | relationship → admins | для ручных |
|
||||
| comment | textarea | |
|
||||
|
||||
### 6.3 `ledger-entries` — Движения по счёту (сайт)
|
||||
|
||||
Только создание. Изменять и удалять нельзя никому, ошибка исправляется встречной записью.
|
||||
|
||||
| Поле | Тип | Описание |
|
||||
|---|---|---|
|
||||
| customer | relationship | |
|
||||
| currency | | валюта аккаунта |
|
||||
| amount | number со знаком | `+` зачисление, `−` списание |
|
||||
| type | select `topup` \| `order_charge` \| `refund` \| `referral_bonus` \| `adjustment` \| `payout` | `payout` — возврат денег с баланса на карту (этап 4) |
|
||||
| payment, order, key, referral | relationship | источник записи |
|
||||
| balanceAfter | number | баланс после записи |
|
||||
| comment | text | обязателен для `adjustment` |
|
||||
| createdBy | relationship → admins, null | null — система |
|
||||
|
||||
Уникальность: (`payment`, `topup`), (`order`, `order_charge`), (`referral`, `customer`, `referral_bonus`). Это защищает от двойного проведения.
|
||||
|
||||
### 6.4 `payment-methods` — Сохранённые способы оплаты (сайт)
|
||||
|
||||
`customer`, `paymentSystem`, `providerMethodId` (зашифрованное), `type`, `brand`, `last4`, `expMonth`, `expYear`, `isDefault`, `status` (`active` \| `detached`), `detachedAt`.
|
||||
|
||||
### 6.5 `referrals` — Приглашения (сайт)
|
||||
|
||||
`referrer`, `referee` (уникальное), `status` (`pending` \| `review` \| `rewarded` \| `rejected`), `qualifyingOrder`, `rewardedAt`, `rejectReason`.
|
||||
|
||||
## 7. Контент и коммуникации
|
||||
|
||||
### 7.1 `pages` — Страницы (сайт)
|
||||
|
||||
`slug` (уникальное в сайте: `terms`, `privacy`, `refund`, `cookies`), `title` (L), `content` (L, Lexical), `seo` (title (L), description (L)). Черновики и версии Payload включены.
|
||||
|
||||
### 7.2 `faq` — Вопросы и ответы (сайт)
|
||||
|
||||
`question` (L), `answer` (L, rich text), `placements` (multi: `home`, `pricing`, `support_connection`, `support_billing`, `support_locations`, `cabinet`, `miniapp`), `sort`, `active`. Один вопрос можно показать в нескольких местах.
|
||||
|
||||
### 7.3 `email-templates` — Шаблоны писем (сайт)
|
||||
|
||||
`code` (уникальное в сайте), `subject` (L), `body` (L, rich text с переменными `{{code}}`, `{{keyTitle}}`, `{{expiresAt}}`, `{{amount}}` …), `active`. Шапка и подвал письма — в коде, для каждого бренда свои.
|
||||
|
||||
Коды шаблонов: `login_code`, `link_email_code`, `order_paid`, `key_provisioning`, `key_ready`, `renewal_reminder`, `expiry_reminder`, `renewal_succeeded`, `renewal_failed`, `grace_started`, `key_expired`, `topup_succeeded`, `referral_reward`, `refund_issued`, `admin_new_order`, `admin_provisioning_needed`, `admin_new_lead`.
|
||||
|
||||
### 7.4 `leads` — Заявки (сайт)
|
||||
|
||||
`type` (`individual_terms` \| `contact`), `name`, `email`, `telegram`, `whatsapp`, `topic`, `message`, `locale`, `pageUrl`, `customer`, `status` (`new` \| `in_progress` \| `done` \| `spam`), `crmId` и `chatwootId` (этап 2), `ipHash`.
|
||||
|
||||
### 7.5 `media` — Файлы
|
||||
|
||||
Загрузки Payload: изображения для контента, иконки платёжных систем, картинки для соцсетей.
|
||||
|
||||
## 8. Журналы
|
||||
|
||||
### 8.1 `email-log` — Журнал писем
|
||||
|
||||
`site`, `to`, `templateCode`, `subject`, `transport` (`smtp` \| `sendmail` \| `log_only`), `status` (`queued` \| `sent` \| `failed`), `error`, `messageId`, `sentAt`, `customer`, `debugCode`.
|
||||
|
||||
`debugCode` — код входа открытым текстом. Заполняется, только если в настройках почты включено «Записывать коды входа в журнал». Видят роли owner и admin. Записи с кодом удаляются через сутки, остальные — через `logRetentionDays`.
|
||||
|
||||
### 8.2 `audit-log` — Журнал действий
|
||||
|
||||
`actor` (администратор или система), `action` (`payment.manual`, `ledger.adjustment`, `key.activate`, `order.refund`, `customer.merge`, …), `entityType`, `entityId`, `before`, `after`, `ipHash`, `createdAt`. Только создание.
|
||||
|
||||
## 9. Глобальные настройки
|
||||
|
||||
### 9.1 `email-settings` — Почта
|
||||
|
||||
| Поле | Описание |
|
||||
|---|---|
|
||||
| transport | `smtp` \| `sendmail` \| `log_only` (только журнал, без отправки) |
|
||||
| smtp | host, port, secure, user, password (зашифрованный) |
|
||||
| sendmailPath | по умолчанию `/usr/sbin/sendmail` |
|
||||
| logLoginCodes | «Записывать коды входа в журнал». В prod по умолчанию выключено; когда включено, в админке висит предупреждение |
|
||||
| logRetentionDays | по умолчанию 30 |
|
||||
| Кнопка | «Отправить тестовое письмо» на указанный адрес |
|
||||
|
||||
## 10. Доступ
|
||||
|
||||
| Коллекция | owner, admin | support | content | пользователь (через приложение) |
|
||||
|---|---|---|---|---|
|
||||
| Настройки и справочники | всё | чтение | чтение | — |
|
||||
| pages, faq, email-templates, локализация | всё | чтение | всё | — |
|
||||
| customers | всё | чтение, правка заметок, блокировка | — | только свой профиль |
|
||||
| keys, connections | всё | всё | — | только свои, чтение |
|
||||
| orders, payments | всё | чтение, ручной платёж | — | только свои, чтение |
|
||||
| ledger-entries | создание корректировок, чтение | чтение | — | только свои, чтение |
|
||||
| leads | всё | всё | — | — |
|
||||
| email-log, audit-log | чтение | чтение без `debugCode` | — | — |
|
||||
|
||||
Пользователь никогда не ходит в REST Payload напрямую: кабинет читает данные на сервере через Local API с `overrideAccess: false` и `user`.
|
||||
@@ -0,0 +1,188 @@
|
||||
# 04. Биллинг
|
||||
|
||||
## 1. Принципы
|
||||
|
||||
1. Всё через счёт. Любой успешный платёж зачисляется на баланс (движение `topup`). Любой заказ оплачивается списанием с баланса (`order_charge`). Поэтому оплата картой, оплата с баланса, доплата разницы и пополнение заранее работают одним механизмом.
|
||||
2. Баланс — это сумма движений по счёту. Поле `customers.balance` — кэш, он обновляется в той же транзакции базы, что и запись движения. Раз в сутки задача `reconcile` сверяет кэш с суммой.
|
||||
3. Движения неизменяемы. Ошибка исправляется встречной записью.
|
||||
4. Одна точка проведения платежа — `core/billing/processPayment(paymentId)`. Её вызывают вебхук провайдера, ручной платёж из админки и тесты. Повторный вызов ничего не делает.
|
||||
5. Суммы — в минимальных единицах валюты аккаунта.
|
||||
|
||||
## 2. Валюта аккаунта
|
||||
|
||||
1. При регистрации валюта аккаунта = валюта, выбранная в переключателе (или валюта по умолчанию домена).
|
||||
2. Сменить валюту в кабинете можно, пока баланс нулевой и нет ключей и неоплаченных заказов. Дальше — через поддержку: администратор пересчитывает вручную корректировками.
|
||||
3. Цены в кабинете всегда в валюте аккаунта. Тариф без цены в этой валюте недоступен.
|
||||
|
||||
## 3. Покупка ключа (оформление заказа на my.*)
|
||||
|
||||
1. Ссылка «Купить» с сайта ведёт на `my.<домен>/{язык}/order?tariff=…&location=…¤cy=…`.
|
||||
2. Шаг «Почта»: email и код ([05-auth.md](05-auth.md)). Вошедший пользователь шаг пропускает.
|
||||
3. Сервер проверяет: тариф активен, есть цена в валюте аккаунта, локация доступна (dedicated: `available` или `on_demand`).
|
||||
4. Создаётся заказ `pending`: `new_key`, сумма — `firstPeriodAmount`, действует 24 часа. Если у пользователя уже есть такой же неоплаченный заказ, используется он.
|
||||
5. Шаг «Оплата»: сводка заказа, согласие с условиями (обязательная галочка со ссылками на юридические страницы) и способ оплаты.
|
||||
a. Баланса хватает — кнопка «Оплатить с баланса», заказ оплачивается сразу.
|
||||
b. Баланса не хватает — список платёжных систем: активные, в валюте аккаунта, с назначением `order`, видимые пользователю. Создаётся платёж `pending` на сумму «стоимость минус баланс» (показываем: «С баланса спишется X, к оплате Y»), дальше — страница или виджет провайдера (этап 4).
|
||||
6. На этапе 1 видимых платёжных систем нет. Заказ создаётся, пользователь видит «Заказ создан, ожидает оплаты». Оплату проводит администратор ручным платежом, либо пользователь оплачивает с баланса, который пополнил администратор. Так проверяется весь сценарий без эквайринга.
|
||||
7. После оплаты: заказ `paid`, ключ создаётся в статусе `provisioning`, письма `order_paid` пользователю и `admin_provisioning_needed` на почту поддержки.
|
||||
|
||||
## 4. Пополнение баланса
|
||||
|
||||
1. Платёж с назначением `topup`. Суммы — пресеты сайта для валюты (`sites.topupPresets`) или своя сумма в пределах лимитов платёжной системы.
|
||||
2. После зачисления — письмо `topup_succeeded`.
|
||||
3. После любого пополнения система сама оплачивает с баланса неоплаченные заказы на продление этого пользователя, начиная со старого. Неоплаченные заказы на новые ключи сама не оплачивает.
|
||||
4. Криптовалюта (этап 4): счёт в монете, курс фиксируется на 15 минут, зачисление в валюте аккаунта после подтверждения сети.
|
||||
|
||||
## 5. Проведение платежа: `processPayment`
|
||||
|
||||
Выполняется, когда платёж переходит в `succeeded`, в одной транзакции:
|
||||
|
||||
1. Заблокировать строку платежа. Если `processedAt` заполнен — выйти.
|
||||
2. Заблокировать строку пользователя.
|
||||
3. Движение `+amount` (`topup`).
|
||||
4. Если назначение `order` и заказ `pending`: баланса хватает — движение `−order.amount` (`order_charge`), заказ `paid`. Не хватает — заказ остаётся `pending`, деньги остаются на балансе.
|
||||
5. Заказ оплачен — исполнить: `new_key` создаёт ключ `provisioning` с копией цены продления, периода и лимита устройств; `renewal` продлевает ключ (раздел 6).
|
||||
6. Проставить `processedAt`.
|
||||
7. Проверить реферальное вознаграждение (раздел 11).
|
||||
8. Поставить в очередь письма, записать журнал действий.
|
||||
|
||||
Если заказ к моменту оплаты отменён или истёк, деньги остаются на балансе, пользователь получает письмо о зачислении.
|
||||
|
||||
Оплата заказа с баланса без внешнего платежа — те же шаги 2, 4, 5, 7, 8.
|
||||
|
||||
## 6. Продление
|
||||
|
||||
### 6.1 Автоматическое (задача `renewals`, каждые 15 минут)
|
||||
|
||||
Берём ключи в статусе `active` или `grace` с `autoRenew = true`, у которых `expiresAt ≤ сейчас + renewalLeadHours` и нет оплаченного заказа на продление с `renewalFor = expiresAt`. Для каждого, с блокировкой ключа:
|
||||
|
||||
1. Найти или создать заказ `renewal`: `renewalFor = expiresAt`, сумма — `renewalAmount` ключа.
|
||||
2. Баланса хватает — списание, заказ `paid`, `expiresAt = renewalFor + period`, статус `active`, письмо `renewal_succeeded`.
|
||||
3. Не хватает, но есть сохранённая карта с автосписанием (Stripe, этап 4) — платёж на разницу без участия пользователя. Неудача — повтор: за 24 часа до окончания, в момент окончания, через 24 и 48 часов (внутри льготного периода). После первой неудачи — письмо `renewal_failed`.
|
||||
4. Нет ни денег, ни карты — заказ ждёт оплаты. Его оплатит пополнение (раздел 4.3) или кнопка «Продлить сейчас».
|
||||
|
||||
### 6.2 «Продлить сейчас»
|
||||
|
||||
Доступна для ключей `active` и `grace`. Создаёт (или берёт существующий) заказ `renewal` на `renewalFor = expiresAt` и оплачивает его с баланса или через платёжную систему. Ключ продлевается на период от текущего `expiresAt`. Можно продлить заранее несколько раз подряд.
|
||||
|
||||
### 6.3 Автопродление и карта
|
||||
|
||||
1. Галочка автопродления — у каждого ключа.
|
||||
2. «Отвязать карту» — в аккаунте, раздел «Счёт и платежи». После отвязки автопродление списывает только баланс.
|
||||
|
||||
## 7. Льготный период и окончание (задача `expiry`, каждые 15 минут)
|
||||
|
||||
1. `active`, срок истёк, продление не оплачено → `grace`, `graceUntil = expiresAt + graceDays`, письмо `grace_started`. Ключ работает.
|
||||
2. Продление оплачено в льготный период → `active`, новый срок считается от старого `expiresAt`: бесплатных дней нет.
|
||||
3. `grace`, `graceUntil` наступил → `expired`, неоплаченный заказ на продление → `expired`, письмо `key_expired`. На этапе 1 ключ попадает в список «К отключению» в админке; отключение автоматическое — этап 5.
|
||||
4. Правило одинаковое для ключей с автопродлением и без. Без автопродления пользователь сам решает, продлевать ли в льготный период.
|
||||
5. Истёкший ключ не продлевается: покупается новый. Администратор может вернуть ключ вручную.
|
||||
|
||||
## 8. Выдача ключа
|
||||
|
||||
1. Админка, ключ в статусе `provisioning`: администратор добавляет подключения (WG/AWG по устройствам, ссылку Happ, RU-адрес), IP-адреса и нажимает «Выдать ключ».
|
||||
2. Нужно хотя бы одно активное подключение. Затем: `active`, `activatedAt = сейчас`, `expiresAt = сейчас + период`, письмо `key_ready`.
|
||||
3. Список «Очередь выдачи» в админке: ключи `provisioning` по времени создания; локации `on_demand` помечены сроком «24 часа».
|
||||
|
||||
## 9. Ручные операции в админке
|
||||
|
||||
| Операция | Что происходит |
|
||||
|---|---|
|
||||
| Ручной платёж | Платёж через платёжную систему `manual`: пользователь, назначение, заказ (необязательно), сумма в валюте аккаунта, статус, комментарий. Когда статус становится `succeeded`, срабатывает `processPayment` — как у настоящего платежа. Можно завести `pending` (ждём перевод) и позже перевести в `succeeded`. Галочки «провести как реальную» нет: проводится всегда |
|
||||
| Корректировка баланса | Движение `adjustment` ±сумма, комментарий обязателен. Цепочка действий не запускается |
|
||||
| Возврат на баланс | На заказе: сумма (не больше оплаченной минус возвращённое), галочка «Отменить ключ». Движение `refund`, заказ `refunded` или `partially_refunded`, ключ `cancelled`, если отмечено. Письмо `refund_issued` |
|
||||
| Выдать ключ | `provisioning` → `active` (раздел 8) |
|
||||
| Продлить вручную | Продлить ключ на N дней без оплаты (компенсация) |
|
||||
| Заблокировать | Пользователь `blocked`: войти нельзя. Ключ `suspended` |
|
||||
|
||||
Все операции пишутся в журнал действий.
|
||||
|
||||
## 10. Возвраты
|
||||
|
||||
1. Политика: если сервер не заработал по нашей вине — полностью в течение первых 7 дней; если пользователь передумал — остаток неиспользованного периода. Сумму считает администратор, система подсказывает остаток пропорционально дням.
|
||||
2. Этап 1 — только возврат на баланс.
|
||||
3. Этап 4 — возврат на исходный способ оплаты, если провайдер умеет. Запись `refund` (+) на баланс по заказу, затем `payout` (−) — деньги уходят на карту. Итог: баланс не меняется, деньги у пользователя на карте.
|
||||
|
||||
## 11. Реферальное вознаграждение
|
||||
|
||||
1. Привязка: при регистрации, если есть cookie `vx_ref` (ставит `my.<домен>/r/<код>`, живёт 60 дней) или параметр `/start <код>` в боте. Код того же сайта, не свой. Создаётся приглашение `pending`.
|
||||
2. Условие: оплачен первый заказ `new_key` приглашённого. Пополнение баланса условием не считается.
|
||||
3. Проверка: совпадает хэш IP регистрации приглашённого и пригласившего → `review`, решает администратор. Иначе — начисление.
|
||||
4. Начисление: движение `referral_bonus` обоим, в валюте каждого, сумма из `sites.referralBonus`. Нет суммы для валюты — бонус не начисляется, в журнале предупреждение. Приглашение `rewarded`, письма `referral_reward`.
|
||||
|
||||
## 12. Уведомления
|
||||
|
||||
| Когда | Шаблон | Условие |
|
||||
|---|---|---|
|
||||
| Заказ оплачен | `order_paid` | всегда |
|
||||
| Ключ ждёт подготовки (24 часа) | `key_provisioning` | локация `on_demand` |
|
||||
| Ключ выдан | `key_ready` | |
|
||||
| За `reminderDays` до окончания | `renewal_reminder` | автопродление включено, но баланса и карты не хватит |
|
||||
| За `reminderDays` до окончания | `expiry_reminder` | автопродление выключено |
|
||||
| Продление прошло | `renewal_succeeded` | |
|
||||
| Карта не прошла | `renewal_failed` | |
|
||||
| Начался льготный период | `grace_started` | |
|
||||
| Ключ истёк | `key_expired` | |
|
||||
| Баланс пополнен | `topup_succeeded` | |
|
||||
| Реферальный бонус | `referral_reward` | |
|
||||
| Возврат | `refund_issued` | |
|
||||
| Администраторам | `admin_new_order`, `admin_provisioning_needed`, `admin_new_lead` | на почту поддержки сайта |
|
||||
|
||||
С этапа 3 те же уведомления дублируются в основного бота, если Telegram привязан.
|
||||
|
||||
## 13. Статусы
|
||||
|
||||
### 13.1 Ключ
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> provisioning: заказ оплачен
|
||||
provisioning --> active: выдан
|
||||
active --> active: продление оплачено
|
||||
active --> grace: срок истёк, оплаты нет
|
||||
grace --> active: продление оплачено
|
||||
grace --> expired: льготный период закончился
|
||||
provisioning --> cancelled: возврат
|
||||
active --> cancelled: возврат с отменой
|
||||
active --> suspended: блокировка
|
||||
suspended --> active: разблокировка
|
||||
```
|
||||
|
||||
### 13.2 Заказ
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> pending
|
||||
pending --> paid
|
||||
pending --> cancelled: отменён
|
||||
pending --> expired: истёк срок заказа или ключ истёк
|
||||
paid --> partially_refunded
|
||||
paid --> refunded
|
||||
partially_refunded --> refunded
|
||||
```
|
||||
|
||||
### 13.3 Платёж
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> pending
|
||||
pending --> succeeded
|
||||
pending --> failed
|
||||
pending --> cancelled
|
||||
succeeded --> partially_refunded
|
||||
succeeded --> refunded
|
||||
```
|
||||
|
||||
## 14. Конкурентность и идемпотентность
|
||||
|
||||
1. Все денежные операции — в транзакции базы. Баланс меняется только под блокировкой строки пользователя (`SELECT … FOR UPDATE`).
|
||||
2. Уникальные ограничения защищают от двойного проведения: движения (см. [03-data-model.md](03-data-model.md), 6.3), платежи (`adapter` + `providerPaymentId`), заказы (`key` + `renewalFor`).
|
||||
3. Вебхуки: проверка подписи, сохранение id события, идемпотентная обработка, быстрый ответ 200.
|
||||
4. Задачи берут ключи с `FOR UPDATE SKIP LOCKED`, чтобы параллельные запуски не продлили ключ дважды.
|
||||
|
||||
## 15. Как показываем в кабинете
|
||||
|
||||
1. История платежей — одна строка на событие для человека. Платёж картой за заказ (зачисление и списание) показывается одной строкой: «Первый месяц · Нидерланды · Амстердам · Карта ···· 4242 · −4 900 ₽». Списание с баланса — «С баланса». Пополнение, реферальный бонус, возврат, корректировка — отдельными строками.
|
||||
2. «Следующее списание» — ближайшее продление среди ключей с автопродлением: сумма, дата, ключ; ниже — следующие.
|
||||
3. Бейдж «Истекает» — до окончания не больше `expiringBadgeDays` и (автопродление выключено или не хватит ни баланса, ни карты).
|
||||
4. На странице ключа: «Продлится автоматически через N дней», если денег или карты хватает, иначе «Осталось N дней» (предупреждающий цвет).
|
||||
@@ -0,0 +1,103 @@
|
||||
# 05. Вход, сессии, аккаунты
|
||||
|
||||
## 1. Аккаунты
|
||||
|
||||
1. Аккаунты раздельные по сайтам ([D-011](decisions/D-011-accounts.md)). Уникальность: (`site`, `email`) и (`site`, `telegramId`).
|
||||
2. Один человек на двух сайтах — два разных пользователя с разными балансами и ключами.
|
||||
3. Администраторы — отдельная коллекция `admins` с паролем, к пользователям отношения не имеют.
|
||||
|
||||
## 2. Вход по коду из письма
|
||||
|
||||
Одно поле email: если аккаунта нет — он создаётся, если есть — выполняется вход. Ответ всегда одинаковый, по нему нельзя узнать, есть ли аккаунт.
|
||||
|
||||
### 2.1 Запрос кода
|
||||
|
||||
1. Пользователь вводит email и проходит ALTCHA.
|
||||
2. Сервер нормализует email (обрезает пробелы, переводит в нижний регистр) и проверяет лимиты:
|
||||
a. по email — не чаще раза в 60 секунд и не больше 5 кодов в час;
|
||||
b. по IP — не больше 20 запросов в час.
|
||||
3. Прошлые неиспользованные коды для этого email и сайта аннулируются.
|
||||
4. Создаётся код: 6 цифр, действует 10 минут. В базе хранится только HMAC-SHA256 (секрет из `.env`, сайт, email, код).
|
||||
5. Письмо `login_code` на языке текущей страницы. Если в настройках почты включено «Записывать коды входа в журнал», код пишется в журнал писем (раздел 4).
|
||||
6. Ответ: «Код отправлен на {email}. Он действует 10 минут». Кнопка «Отправить ещё раз» — через 60 секунд, «Другая почта» — назад.
|
||||
|
||||
### 2.2 Проверка кода
|
||||
|
||||
1. Берётся последний действующий код для email и сайта. Счётчик попыток +1; после 5 неудачных код аннулируется.
|
||||
2. Сравнение — за постоянное время.
|
||||
3. Код верный:
|
||||
a. найти пользователя (сайт, email) или создать: `registeredVia = email`, язык — текущий, валюта — выбранная (Q-04), реферальная привязка из cookie `vx_ref`, хэш IP регистрации;
|
||||
b. пользователь `blocked` — отказ: «Аккаунт заблокирован, напишите в поддержку»;
|
||||
c. проставить `emailVerifiedAt` и `lastLoginAt`, создать сессию, вернуть на страницу, с которой пришёл (кабинет или оформление заказа).
|
||||
|
||||
## 3. Сессии
|
||||
|
||||
1. Cookie `vx_session`: случайные 32 байта, только для хоста кабинета (`my.veilix.app` и т.д., без атрибута Domain), `HttpOnly`, `Secure`, `SameSite=Lax`.
|
||||
2. В базе (`customer-sessions`) — только SHA-256 токена. Срок — 30 дней, продлевается при активности (не чаще раза в сутки).
|
||||
3. «Выйти» отзывает текущую сессию, «Выйти на всех устройствах» — все.
|
||||
4. Payload: коллекция `customers` — коллекция авторизации с `disableLocalStrategy: true` и своей стратегией. Стратегия читает cookie, находит сессию и возвращает пользователя. Поэтому правила доступа Payload работают и для пользователей.
|
||||
5. Защита от CSRF: `SameSite=Lax`; server actions Next.js сверяют Origin и Host; свои POST-эндпоинты проверяют Origin.
|
||||
6. Cookie хостовые, поэтому dev и prod не пересекаются: `dev.my.veilix.app` и `my.veilix.app` не видят cookie друг друга.
|
||||
|
||||
## 4. Коды в журнале для отладки
|
||||
|
||||
1. Глобальная настройка «Почта» → «Записывать коды входа в журнал».
|
||||
2. Включено: в записи журнала писем видно поле `debugCode` с кодом открытым текстом. В dev-логе процесса код тоже печатается.
|
||||
3. Видят только роли owner и admin. Пока настройка включена, в админке висит предупреждение. Записи с кодом удаляются через сутки.
|
||||
4. Вместе с транспортом «только журнал» (письма не отправляются, только пишутся в журнал) это позволяет тестировать вход без настроенной почты.
|
||||
5. В prod по умолчанию выключено: с кодами из журнала можно войти в любой аккаунт.
|
||||
|
||||
## 5. Оформление заказа
|
||||
|
||||
Шаг «Почта» на странице заказа — тот же вход по коду. После подтверждения пользователь остаётся на шаге «Оплата».
|
||||
|
||||
## 6. Telegram (этап 3)
|
||||
|
||||
### 6.1 Вход из MiniApp
|
||||
|
||||
1. MiniApp передаёт серверу `initData` от Telegram.
|
||||
2. Сервер проверяет подпись: HMAC-SHA256 с ключом `HMAC_SHA256("WebAppData", токен основного бота)`, `auth_date` не старше суток.
|
||||
3. Ищем пользователя (сайт, telegramId). Нет — создаём: `registeredVia = telegram`, язык — из `language_code` Telegram (если он среди включённых), валюта — по умолчанию домена.
|
||||
4. Сессия `kind = miniapp`. Как её хранить (cookie внутри WebView Telegram или токен в памяти) — решаем на этапе 3.
|
||||
|
||||
### 6.2 Привязка почты из Telegram
|
||||
|
||||
1. Пользователь вводит email в боте или MiniApp и получает код с назначением `link_email`.
|
||||
2. Код верный — три случая:
|
||||
a. на этом сайте нет пользователя с такой почтой — почта привязывается, теперь работает вход на сайте по коду;
|
||||
b. есть пользователь с этой почтой без Telegram — аккаунты объединяются (раздел 6.4);
|
||||
c. у пользователя с этой почтой привязан другой Telegram — автоматически не объединяем: «Эта почта привязана к другому Telegram-аккаунту, напишите в поддержку».
|
||||
|
||||
### 6.3 Привязка Telegram с сайта и вход через Telegram
|
||||
|
||||
1. «Подключить Telegram» в кабинете создаёт одноразовый токен (10 минут) и открывает `t.me/<основной бот>?start=link_<токен>`. Бот проверяет токен и привязывает Telegram к аккаунту. Если этот Telegram уже у другого пользователя сайта — правила раздела 6.4.
|
||||
2. «Войти через Telegram» на сайте — официальный виджет Telegram Login. Для основного бота в BotFather указывается домен кабинета. Нет пользователя с этим Telegram — создаётся новый.
|
||||
|
||||
### 6.4 Объединение аккаунтов (в пределах сайта)
|
||||
|
||||
1. Основным остаётся аккаунт с почтой, к нему переходит Telegram второго.
|
||||
2. Ключи, заказы, платежи, сохранённые карты, заявки переназначаются на основной аккаунт.
|
||||
3. Баланс переносится парой движений `adjustment` с комментарием «Объединение аккаунтов»: минус на втором, плюс на основном. Сами движения не меняются.
|
||||
4. Реферальная привязка: если у обоих есть пригласивший — остаётся более ранний. Реферальный код — основного аккаунта.
|
||||
5. Второй аккаунт блокируется со ссылкой «объединён с …». Всё пишется в журнал действий.
|
||||
6. Конфликты переводят объединение в ручной режим, его делает администратор кнопкой «Объединить аккаунты»: разные валюты при ненулевых балансах, у обоих привязан Telegram.
|
||||
|
||||
## 7. Администраторы
|
||||
|
||||
1. Стандартная авторизация Payload: email и пароль, cookie только на хосте админки.
|
||||
2. Админка открывается только на `plc.veilix.cc` и `dev.plc.veilix.cc`; nginx пускает туда только с разрешённых IP.
|
||||
3. 5 неудачных попыток — блокировка на 15 минут.
|
||||
4. Первый администратор каждого окружения создаётся на экране первого входа Payload.
|
||||
|
||||
## 8. Cookies
|
||||
|
||||
| Cookie | Где | Срок | Зачем |
|
||||
|---|---|---|---|
|
||||
| `vx_session` | хост кабинета | 30 дней | сессия пользователя |
|
||||
| `vx_ref` | хост кабинета | 60 дней | реферальный код |
|
||||
| `NEXT_LOCALE` | каждый хост | 1 год | выбранный язык |
|
||||
| `vx_currency` | хост сайта и кабинета | 1 год | выбранная валюта (у вошедшего — валюта аккаунта) |
|
||||
| `vx_consent` | хост сайта и кабинета | 1 год | согласие на cookies (этап 1h) |
|
||||
| `payload-token` | хост админки | сессия админки | вход администратора |
|
||||
|
||||
Тема (светлая, тёмная, как в системе) хранится в localStorage, как в макете, и применяется скриптом до отрисовки, без мигания.
|
||||
@@ -0,0 +1,79 @@
|
||||
# 06. Языки и валюты на сайте
|
||||
|
||||
## 1. Языки
|
||||
|
||||
1. Сейчас: `en` (по умолчанию), `ru`, `de`, `fr`, `es`.
|
||||
2. Позже: `pt-br` (бразильский португальский) и `ja`. Для японского понадобится запасной шрифт: Commissioner и Unbounded не содержат японских знаков.
|
||||
3. Список языков задаётся в конфиге Payload и next-intl (код). У каждого домена в базе — включённые языки и язык по умолчанию.
|
||||
4. Интерфейс админки — `ru` и `en`.
|
||||
|
||||
## 2. Два слоя переводов
|
||||
|
||||
| Что | Где | Кто правит |
|
||||
|---|---|---|
|
||||
| Интерфейс: кнопки, подписи, ошибки, сообщения форм | `src/i18n/messages/{en,ru,de,fr,es}.json`, next-intl | разработка, через git |
|
||||
| Контент: тарифы, локации, FAQ, страницы, шаблоны писем, тексты бота | локализованные поля Payload | админка, без деплоя |
|
||||
|
||||
1. В коде компонентов нет строк на конкретном языке, только ключи переводов.
|
||||
2. Нет перевода на языке — показывается английский. Отдельная проверка выводит список непереведённых ключей.
|
||||
3. Множественные числа — ICU в next-intl: «12 серверов», «3 сервера», «1 сервер».
|
||||
|
||||
## 3. URL
|
||||
|
||||
1. Английский без префикса, остальные — с префиксом: `veilix.app/pricing`, `veilix.app/ru/pricing`, `veilix.app/de/pricing`.
|
||||
2. Так же в кабинете: `my.veilix.app/keys`, `my.veilix.app/ru/keys`.
|
||||
3. MiniApp: адрес в BotFather один, поэтому язык берётся из профиля пользователя или из Telegram; префикс в URL необязателен.
|
||||
4. Пути одинаковые для всех языков (английские): `/pricing`, `/locations`, `/support`, `/legal/terms`, `/keys`, `/billing`, `/referral`, `/order`, `/login`.
|
||||
5. Язык, не включённый на домене, — 404.
|
||||
|
||||
## 4. Определение языка
|
||||
|
||||
1. Порядок: язык из URL → выбранный ранее (cookie `NEXT_LOCALE`, у вошедшего — язык профиля) → язык браузера (`Accept-Language`) среди включённых → страна из заголовка nginx `X-Geo-Country` (MaxMind) → `en`.
|
||||
2. Перенаправление на версию с префиксом — только при первом заходе на адрес без префикса, без cookie, и только если выбран не английский. Поисковых ботов не перенаправляем никогда: так рекомендует Google.
|
||||
3. Переключатель языка ставит cookie `NEXT_LOCALE`. У вошедшего пользователя язык сохраняется ещё и в профиле: на нём уходят письма и сообщения бота.
|
||||
4. Соответствие стран языкам по умолчанию: RU, BY, KZ, KG → ru; DE, AT → de; FR → fr; ES и страны Латинской Америки → es; остальные → en.
|
||||
|
||||
## 5. SEO
|
||||
|
||||
1. `hreflang` на все включённые языки и `x-default` на английскую версию.
|
||||
2. `canonical` на основной домен.
|
||||
3. `sitemap.xml` для каждого маркетингового домена с вариантами всех языков.
|
||||
4. Кабинет и MiniApp не индексируются (`noindex`).
|
||||
5. Атрибут `lang` у `<html>` соответствует языку страницы.
|
||||
|
||||
## 6. Валюта
|
||||
|
||||
1. Переключатель валюты отдельный от языка, стоит рядом с переключателем языка в шапке и подвале. В макете его нет — добавляется в стиле переключателя языка.
|
||||
2. Валюта не определяется по стране: из России часто заходят через VPN.
|
||||
3. По умолчанию — валюта домена для языка (`domains.localeCurrency`: ru → RUB), иначе `domains.defaultCurrency` (USD).
|
||||
4. Выбор хранится в cookie `vx_currency`. Валюта не входит в URL: маркетинговые страницы заранее собраны во всех валютах, proxy выбирает вариант по cookie. Поисковые боты видят валюту по умолчанию.
|
||||
5. Ссылка «Купить» передаёт валюту в оформление заказа (`?currency=`). У вошедшего пользователя в кабинете всегда валюта аккаунта ([04-billing.md](04-billing.md), раздел 2).
|
||||
6. Форматирование — `Intl.NumberFormat` с узким символом валюты: «4 900 ₽» в русском, «₽4,900» в английском, «$49». Неразрывные пробелы ставит Intl. Даты — `Intl.DateTimeFormat`: «14 октября 2026».
|
||||
|
||||
## 7. Как переводим
|
||||
|
||||
1. Источник: русские тексты макетов и английские из словаря макета `design/handoff/project/i18n.js` (около 515 строк). Они ложатся в `ru.json` и `en.json` и в контент Payload.
|
||||
2. Немецкий, французский, испанский — машинный перевод LLM с глоссарием на этапе 1h. Вычитки носителями нет, поэтому глоссарий и контекст обязательны.
|
||||
3. В юридических текстах пункт: «При расхождениях между версиями действует английская».
|
||||
4. Новая строка интерфейса: разработчик добавляет ключ в `en.json` и `ru.json`; de, fr, es переводятся пакетно перед выкладкой.
|
||||
|
||||
## 8. Глоссарий
|
||||
|
||||
| RU | EN | Примечание |
|
||||
|---|---|---|
|
||||
| Вейликс / ВейликсВПН | Veilix / VeilixVPN | не переводится |
|
||||
| ключ | key | купленная единица доступа |
|
||||
| локация | location | |
|
||||
| тариф | plan | |
|
||||
| подключение | connection | |
|
||||
| устройство | device | |
|
||||
| индивидуальный IP | dedicated IP | |
|
||||
| RU-адрес | RU address | |
|
||||
| баланс, счёт | balance | |
|
||||
| пополнить | top up | |
|
||||
| продление, автопродление | renewal, auto-renewal | |
|
||||
| льготный период | grace period | |
|
||||
| реферальная программа | referral program | |
|
||||
| установим за 24 часа | ready within 24 hours | статус локации dedicated |
|
||||
|
||||
Колонки для de, fr, es добавляются на этапе 1h.
|
||||
@@ -0,0 +1,104 @@
|
||||
# 07. Фронтенд
|
||||
|
||||
## 1. Макеты
|
||||
|
||||
1. Источник — [design/handoff/project/](../design/handoff/project/), экспорт Claude Design. Прототипы на HTML: разметка внутри `<x-dc>`, логика в `<script data-dc-script>`, стили — inline плюс CSS-переменные в `<style>`. `support.js` — среда выполнения прототипа, в продукт не переносится.
|
||||
2. Задача — воспроизвести внешний вид точно, читая код макетов: отступы, размеры, цвета, состояния. Внутреннюю структуру прототипа не копируем.
|
||||
3. Тексты макетов — на русском, английские версии — в `i18n.js` (словарь «русская строка → английская»).
|
||||
4. Бриф дизайна — `design/handoff/project/uploads/` (файл «Дизайн VEILIX.md»). Где бриф расходится с прототипами или с [01-product.md](01-product.md), верны прототипы и 01-product.
|
||||
|
||||
## 2. Какой макет куда
|
||||
|
||||
| Макет | Вейликс | ВейликсВПН |
|
||||
|---|---|---|
|
||||
| `Main` / `MassMain` | veilix.app `/` | veilixvpn.com `/` |
|
||||
| `Price` / `MassPrice` | `/pricing` | `/pricing` |
|
||||
| `Locations` / `MassLocations` | `/locations` | `/locations` |
|
||||
| `Support` / `MassSupport` | `/support` | `/support` |
|
||||
| `Order` / `MassOrder` | my.veilix.app `/order` | my.veilixvpn.com `/order` |
|
||||
| `App` / `MassApp` | my.veilix.app: `/login`, `/`, `/keys`, `/keys/[id]`, `/buy`, `/billing`, `/referral` | то же на my.veilixvpn.com |
|
||||
| `AppTG` / `MassAppTG` | tg.veilix.app (этап 3) | tg.veilixvpn.com (этап 3) |
|
||||
|
||||
Плюс страницы без макета: `/legal/terms`, `/legal/privacy`, `/legal/refund`, `/legal/cookies`, 404 и 500 — в стиле сайта.
|
||||
|
||||
## 3. Маршруты кабинета
|
||||
|
||||
| Путь | Что |
|
||||
|---|---|
|
||||
| `/login` | вход по коду |
|
||||
| `/` | главная: ключи, «Купить ключ», FAQ |
|
||||
| `/keys`, `/keys/[id]` | список ключей, страница ключа: срок, автопродление, «Продлить сейчас», WG-конфиги (скачать, QR), ссылка Happ/Xray (копировать, QR) |
|
||||
| `/buy` | покупка внутри кабинета: dedicated — сетка локаций с ценами, mass — «Месяц» и «Год» |
|
||||
| `/order` | оформление заказа: шаги «Почта» и «Оплата», сводка |
|
||||
| `/billing` | баланс, пополнение, карта, ближайшие списания, история |
|
||||
| `/referral` | реферальная программа |
|
||||
| `/r/[код]` | реферальная ссылка: ставит cookie и ведёт на сайт бренда |
|
||||
|
||||
Навигация по макету: на широком экране — боковая панель, на узком — плавающая панель вкладок «Главная, Ключи, Купить, Ещё».
|
||||
|
||||
## 4. Темы и токены
|
||||
|
||||
1. Tailwind v4: токены макета — CSS-переменные, подключённые в `@theme`. Бренд задаёт атрибут `data-site="dedicated|mass"` на `<html>`, тему — `data-theme="light|dark"`. Режим «Как в системе» следует `prefers-color-scheme`.
|
||||
2. Переключатель темы — 3 режима, как в макете. Выбор хранится в localStorage, тема применяется скриптом в `<head>` до отрисовки.
|
||||
3. Токены в OKLCH, как в макете. Ниже — для ориентира; точные значения — в `<style>` прототипов, при расхождении верен прототип.
|
||||
|
||||
| Токен | Вейликс светлая | Вейликс тёмная | ВейликсВПН светлая | ВейликсВПН тёмная |
|
||||
|---|---|---|---|---|
|
||||
| `--bg` | 99% .005 296 (#fcfbff) | 17% .022 296 (#100d18) | 99.6% .0015 215 | 23% .026 210 |
|
||||
| `--surface` | 100% 0 0 | 21% .026 296 | 100% 0 0 | 26.5% .028 210 |
|
||||
| `--surface-2` | 97.5% .01 296 | 24% .03 296 | 98.4% .003 215 | 30% .03 210 |
|
||||
| `--ink` | 24% .04 296 (#211b30) | 96% .01 296 | 24% .04 204 | 96% .012 196 |
|
||||
| `--ink-2` | 45% .03 296 | 79% .02 296 | 45% .03 204 | 80% .02 196 |
|
||||
| `--ink-3` | 60% .025 296 | 63% .025 296 | 60% .025 204 | 65% .025 196 |
|
||||
| `--border` | 92% .014 296 | 31% .032 296 | 92% .014 200 | 36% .04 204 |
|
||||
| `--accent` | 45% .17 296 (#6137a6) | 74% .15 296 (#b495fe) | 48% .105 178 (#00705e) | 76% .105 190 (#52c6c0) |
|
||||
| `--accent-hover` | 38% .17 296 | 80% .13 296 | 41% .1 178 | 83% .09 190 |
|
||||
| `--accent-soft` | 95.5% .028 296 | 27% .05 296 | 95.5% .032 180 | 31% .05 190 |
|
||||
| `--on-accent` | 99% .01 296 | 17% .03 296 | 99% .01 180 | 20% .04 204 |
|
||||
| `--glow-1/2/3` | 86% .1 296 / 89% .09 266 / 91% .08 332 | 45% / 40% / 42% | 86% .085 190 / 88% .075 205 / 89% .07 172 | 52% .09 200 / 47% .085 215 / 50% .08 180 |
|
||||
| `--sel` | — | — | 96.5% .016 190 | 30% .03 195 |
|
||||
|
||||
Общие для брендов: `--warn` (62% .14 62 / 78% .13 62), `--tg` (62% .15 250 / 70% .13 250), `--veil` (белый 55% / 7%), `--shadow`, цвета карты `--land`, `--land-line`, `--ocean`.
|
||||
|
||||
## 5. Шрифты, иконки, логотип
|
||||
|
||||
1. Commissioner 300–600 — весь текст; Unbounded 600 — только надпись логотипа. Подключение через `next/font`: файлы скачиваются при сборке и отдаются с нашего домена, без запросов к Google Fonts. Наборы символов: latin, latin-ext, cyrillic.
|
||||
2. Иконки, флаги и логотип в макете нарисованы CSS-фигурами. Переносим их React-компонентами, сохраняя вид.
|
||||
3. Логотип — вариант «Слои». Фавиконы — `favicon.svg` и `favicon-mass.svg` из макета.
|
||||
|
||||
## 6. Карта мира
|
||||
|
||||
1. В макете карта строится в браузере через d3 и topojson с загрузкой данных с CDN. В продукте SVG-карта рендерится на сервере при сборке: d3-geo, проекция Natural Earth, данные `world-atlas` (countries-110m) из npm.
|
||||
2. В браузере — только интерактив: подсветка, всплывающая карточка, синхронизация с сеткой локаций.
|
||||
3. На узком экране карта прокручивается горизонтально, как в макете.
|
||||
|
||||
## 7. Решения по расхождениям в макетах
|
||||
|
||||
1. Устройств — 5, это поле тарифа. Цена продления dedicated — 2 499 ₽, цены задаются тарифами (3 уровня).
|
||||
2. Оформление заказа получает тариф, локацию и валюту из ссылки. В макете там всегда Нидерланды и 4 900 ₽.
|
||||
3. Поля номера карты из макета `Order` не делаем: карта вводится в виджете провайдера (этап 4). На этапе 1 платёжных систем для пользователя нет ([04-billing.md](04-billing.md), раздел 3.6).
|
||||
4. Mass: в кабинете «Список ключей», без «текущей/запасной локации» и без экрана смены страны. Внутри ключа — ссылка Xray и WG-конфиги по странам.
|
||||
5. Статус локации показываем везде одинаково: в списках, карточках и на карте.
|
||||
6. Кнопка «Получить VPN» на главной ведёт к блоку цен на этой же странице, на остальных страницах — на `/pricing`.
|
||||
7. FAQ — одна коллекция с местами показа: главная, цены, поддержка (3 раздела), кабинет, MiniApp.
|
||||
8. Контакты поддержки берутся из настроек сайта, Telegram — ссылка на бота поддержки.
|
||||
9. Плашки способов оплаты в подвале — по активным платёжным системам сайта.
|
||||
10. Добавляем то, чего нет в макете: переключатель валюты, `<title>` и мета-описания, H1 на каждой странице, поддержку `prefers-reduced-motion` для анимаций.
|
||||
11. Показывать ли на сайте количество свободных серверов (в MiniApp макет его показывает) — решаем на этапе 1b. По умолчанию — только статус.
|
||||
|
||||
## 8. Состояния, которых нет в макетах
|
||||
|
||||
Рисуем в стиле макета на соответствующих этапах:
|
||||
|
||||
1. Вход: ошибка кода, истёкший код, таймер повторной отправки, загрузка.
|
||||
2. Ключ: готовится (до 24 часов), льготный период, истёк, заблокирован; пустой список ключей; ключ без подключений.
|
||||
3. Заказ: «ожидает оплаты» (этап 1), не хватает баланса, оплата не прошла; 3-D Secure и счёт в криптовалюте — этап 4.
|
||||
4. Счёт: своя сумма пополнения, подтверждение отвязки карты, результат пополнения.
|
||||
5. Общие: уведомления (тосты), скелетоны загрузки, страницы 404 и 500, ошибки валидации форм.
|
||||
|
||||
## 9. Производительность и доступность
|
||||
|
||||
1. Маркетинговые страницы: Lighthouse mobile ≥ 90 (цель 95+), LCP ≤ 2 с, CLS ≤ 0.05, INP ≤ 200 мс.
|
||||
2. JavaScript в браузере — только интерактивные островки: переключатели языка, валюты и темы, FAQ, выбор локации, формы, карта.
|
||||
3. Сторонние скрипты при первой загрузке — только Umami (асинхронно). ALTCHA подгружается только на формах.
|
||||
4. Доступность — WCAG 2.1 AA: контраст, видимый фокус, работа с клавиатуры для выпадающих списков, аккордеонов и шитов, области нажатия ≥ 44 px.
|
||||
@@ -0,0 +1,82 @@
|
||||
# 08. Интеграции
|
||||
|
||||
## 1. Почта (этап 1c)
|
||||
|
||||
1. Шаблоны — в Payload (`email-templates`): тема и текст на каждом языке, для каждого сайта свои. Шапка (логотип) и подвал (контакты, юридические ссылки) — в коде, у каждого бренда свои.
|
||||
2. Отправка — через email-адаптер Payload на Nodemailer. Транспорт переключается в админке (глобал «Почта»):
|
||||
a. `smtp` — внешний SMTP-провайдер (данные в настройках, пароль зашифрован);
|
||||
b. `sendmail` — локальный Postfix (`/usr/sbin/sendmail`);
|
||||
c. `log_only` — не отправлять, только записать в журнал (для разработки).
|
||||
3. Письма уходят через фоновую задачу `send-email` с повторами при ошибке. Каждое письмо пишется в журнал писем; коды входа — по флагу ([05-auth.md](05-auth.md), раздел 4).
|
||||
4. Кнопка «Отправить тестовое письмо» проверяет текущий транспорт.
|
||||
5. Чтобы письма через Postfix не попадали в спам (root, [09-infrastructure.md](09-infrastructure.md), раздел 6):
|
||||
a. Postfix отправляет с IP 169.58.76.183 под именем `fr-ub2gs3.veilix.cc` — у этого IP прямая и обратная DNS-записи совпадают;
|
||||
b. для veilix.app и veilixvpn.com нужны SPF, DKIM (OpenDKIM уже установлен) и DMARC;
|
||||
c. проверить, что исходящий порт 25 открыт и IP нет в чёрных списках.
|
||||
6. Новые IP почтовые сервисы поначалу принимают настороженно, поэтому SMTP-провайдер держим запасным вариантом.
|
||||
|
||||
## 2. ALTCHA (этап 1c)
|
||||
|
||||
1. Капча на proof-of-work без сторонних сервисов: браузер решает небольшую вычислительную задачу, сервер проверяет подпись.
|
||||
2. Эндпоинт выдачи задачи — `/svc/altcha/challenge`, ключ подписи — в `.env`.
|
||||
3. Где: запрос кода входа, формы заявок и обратной связи.
|
||||
4. Вместе с лимитами по IP и email ([05-auth.md](05-auth.md)). Провайдер капчи изолирован в `core/`, его можно заменить.
|
||||
|
||||
## 3. MaxMind (этап 1a)
|
||||
|
||||
1. Модуль nginx `geoip2` (уже установлен) определяет страну по базе GeoLite2-Country и передаёт её приложению заголовком `X-Geo-Country`. Клиентский заголовок с таким именем nginx перезаписывает.
|
||||
2. База обновляется `geoipupdate` по таймеру (root, нужен бесплатный аккаунт MaxMind). Модуль nginx перечитывает базу сам (`auto_reload`).
|
||||
3. Страна используется только для выбора языка по умолчанию, не для валюты.
|
||||
4. Если понадобится страна произвольного IP (антифрод), приложение читает тот же `.mmdb` библиотекой `maxmind`.
|
||||
|
||||
## 4. Chatwoot (этап 2)
|
||||
|
||||
1. Один экземпляр на отдельном VPS обслуживает оба бренда. У каждого бренда свои входящие каналы:
|
||||
a. виджет на сайте и в кабинете;
|
||||
b. бот поддержки в Telegram, подключённый к Chatwoot напрямую как Telegram-канал.
|
||||
2. Для вошедших пользователей виджет получает идентификацию с HMAC-подписью: оператор видит, кто пишет, а подделать её нельзя. Передаём атрибуты: сайт, id пользователя, email, число ключей.
|
||||
3. Почему бот поддержки отдельный: Telegram отдаёт обновления одного бота только одному получателю. Основной бот принадлежит нашему приложению, бот поддержки — Chatwoot.
|
||||
|
||||
## 5. Twenty CRM (этап 2)
|
||||
|
||||
1. Заявки из `leads` уходят в Twenty через API: человек и сделка с полями заявки, брендом и страницей. Отправка — фоновой задачей с повторами; id в Twenty сохраняется в `leads.crmId`.
|
||||
2. До этапа 2 заявки видны в админке Payload и приходят письмом `admin_new_lead` на почту поддержки.
|
||||
|
||||
## 6. Telegram (этап 3)
|
||||
|
||||
1. Боты для каждого бренда:
|
||||
a. основной: команды, кнопка MiniApp, уведомления. Код — grammY в нашем приложении, вебхук `/svc/telegram/<сайт>/webhook` с секретным токеном;
|
||||
b. поддержки: подключён к Chatwoot.
|
||||
Для dev — отдельные тестовые боты.
|
||||
2. MiniApp — на tg.*, вход по `initData` ([05-auth.md](05-auth.md), раздел 6). Dev-MiniApp доступна только Telegram-аккаунтам из списка разрешённых.
|
||||
3. Черновик команд основного бота: `/start` (в том числе с реферальным кодом или токеном привязки), `/keys`, `/buy`, `/balance`, `/topup`, `/referral`, `/language`, `/support` (ссылка на бота поддержки), `/help`. Тексты и кнопки — в Payload, локализованные.
|
||||
4. Уведомления из [04-billing.md](04-billing.md) (раздел 12) дублируются в бот, если Telegram привязан.
|
||||
5. Оплата в Telegram (Q-11): по правилам Telegram цифровые товары в ботах и MiniApp оплачиваются Telegram Stars, за нарушения ботов блокируют. Варианты: Stars, внешняя страница оплаты, покупки в Telegram только с баланса. Решаем на старте этапа.
|
||||
|
||||
## 7. Платёжные системы
|
||||
|
||||
1. Каждый провайдер — адаптер в `core/payments/` с единым интерфейсом:
|
||||
a. `createPayment` — создать платёж и вернуть адрес страницы оплаты или данные виджета;
|
||||
b. `handleWebhook` — проверить подпись и вернуть новый статус платежа;
|
||||
c. по возможностям: `chargeSavedMethod` (автосписание), `refund`, `detachMethod`.
|
||||
2. Запись в коллекции `payment-systems` включает адаптер в валютах и для назначений ([03-data-model.md](03-data-model.md), 3.4). На каком сайте система доступна, следует из валют его доменов. Ключи доступа — в `.env`.
|
||||
3. Этап 1: только `manual` — для администраторов.
|
||||
4. Этап 4:
|
||||
a. Stripe — USD: карты, сохранение карты, автосписания без участия пользователя на переменную сумму («цена минус баланс»), без Stripe Subscriptions. Сначала тестовый режим;
|
||||
b. RUB — процессинги, принимающие карты РФ (вероятно, KZ и TR), только пополнения. Провайдеров выбираем на этапе (Q-12);
|
||||
c. криптовалюта — пополнения в RUB и USD, провайдер выбирается на этапе;
|
||||
d. ЮKassa — опционально, только пополнения, если примет категорию.
|
||||
5. Вебхуки — `/svc/payments/<адаптер>/webhook`; обработка идемпотентная ([04-billing.md](04-billing.md), раздел 14).
|
||||
|
||||
## 8. Umami и баннер cookies (этап 1h)
|
||||
|
||||
1. Umami — своя аналитика без cookies, данные не уходят третьим лицам ([D-018](decisions/D-018-analytics.md)).
|
||||
a. Ставится на этот сервер из исходников, без Docker: Node.js и PostgreSQL здесь уже есть. Umami 3.x поддерживает только PostgreSQL.
|
||||
b. Отдельный пользователь ОС `umami`, своя база `umami`, systemd user-юнит, порт 3300.
|
||||
c. Панель — `umi.veilix.cc`, доступ только с разрешённых IP.
|
||||
2. Скрипт и отправка событий проксируются через домены брендов под нейтральными путями (переменные Umami `TRACKER_SCRIPT_NAME` и `COLLECT_API_ENDPOINT`), чтобы их не резали блокировщики рекламы. Реальный IP посетителя Umami берёт из заголовка nginx (`CLIENT_IP_HEADER`). Телеметрия Umami выключена (`DISABLE_TELEMETRY=1`).
|
||||
3. Баннер cookies: обязательные (сессия, язык, валюта) и функциональные (чат Chatwoot). Решение пользователя хранится в cookie `vx_consent`, необязательные скрипты загружаются только после согласия. Библиотека — vanilla-cookieconsent или своя лёгкая реализация.
|
||||
|
||||
## 9. Автоматизация ключей (этап 5)
|
||||
|
||||
Подбор свободного сервера по тарифу (страны, теги) и автоматическая выдача подключений. Обсуждаем при старте этапа.
|
||||
@@ -0,0 +1,108 @@
|
||||
# 09. Инфраструктура
|
||||
|
||||
## 1. Сервер (проверено 18–20.10.2026)
|
||||
|
||||
| Что | Значение |
|
||||
|---|---|
|
||||
| ОС | Ubuntu 26.04.1 LTS |
|
||||
| Ресурсы | 4 vCPU, 7.7 ГБ RAM, 8 ГБ swap, 145 ГБ диска (занято 24 ГБ) |
|
||||
| IP | 169.58.76.183 — сайты (PTR `fr-ub2gs3.veilix.cc`, прямая запись совпадает); 173.212.216.183 — основной, через него по умолчанию идёт исходящий трафик (PTR `vmi3593751.contaboserver.net`) |
|
||||
| Установлено | Node 24.22, PostgreSQL 18.6, nginx 1.28 (модули geoip2 и brotli подключены), Redis 8, Postfix 3.10, OpenDKIM, certbot 4.0, Docker 29 (не используем), git |
|
||||
| Нет | pnpm, база GeoLite2 (`.mmdb`), сертификаты TLS, geoipupdate |
|
||||
| DNS | все 15 хостов проекта (вместе с `umi.veilix.cc`) указывают на 169.58.76.183; DNS-хостинг — Gcore |
|
||||
|
||||
## 2. Папки
|
||||
|
||||
```
|
||||
/home/vx_dev/veilix_dev/ — dev (пользователь vx_dev): рабочая копия git, .env, media/
|
||||
/home/vx_prod/veilix/ — prod (пользователь vx_prod), закрыто для остальных
|
||||
releases/<дата-тег>/ — собранные релизы (храним 5 последних)
|
||||
current -> releases/… — симлинк на работающий релиз
|
||||
shared/.env — секреты prod
|
||||
shared/media/ — загруженные файлы prod
|
||||
/home/umami/umami/ — Umami (пользователь umami)
|
||||
```
|
||||
|
||||
vx — личный пользователь администратора, к проекту отношения не имеет. Под vx_prod нет редактора и ИИ-агентов, VSCode Remote под vx_prod не подключаем ([D-004](decisions/D-004-environments.md)). Домашние папки всех пользователей — `750`, владелец — сам пользователь. Пустые папки `/home/vx/veilix.app`, `/home/vx/my.veilix.app`, `/home/vx/tg.veilix.app` проекту не нужны.
|
||||
|
||||
## 3. Процессы
|
||||
|
||||
1. Каждый сервис — под своим пользователем ОС, как systemd user-юнит. Root один раз включает `loginctl enable-linger` для vx_dev, vx_prod и umami, чтобы юниты работали без входа в систему.
|
||||
2. `veilix-dev.service` (vx_dev): `pnpm dev` на порту 3100 из `/home/vx_dev/veilix_dev`, перезапуск при сбое.
|
||||
3. `veilix.service` (vx_prod): `pnpm start` на порту 3000 из `/home/vx_prod/veilix/current`, `NODE_ENV=production`, перезапуск всегда.
|
||||
4. `umami.service` (umami): Umami на порту 3300.
|
||||
5. Фоновые задачи Payload выполняются внутри процессов приложения ([02-architecture.md](02-architecture.md), раздел 7).
|
||||
6. Логи — journald, у каждого пользователя свои (`journalctl --user -u veilix` под vx_prod). Сессии Claude логи prod не видят: при разборе проблем prod их присылает пользователь.
|
||||
7. Проверка живости — `/svc/health` (с проверкой базы).
|
||||
|
||||
## 4. nginx
|
||||
|
||||
| Хосты | Куда | Доступ |
|
||||
|---|---|---|
|
||||
| veilix.app, veilixvpn.com, my.veilix.app, my.veilixvpn.com, tg.veilix.app, tg.veilixvpn.com | 127.0.0.1:3000 | открыт |
|
||||
| plc.veilix.cc | 127.0.0.1:3000 | только разрешённые IP |
|
||||
| dev.veilix.app, dev.my.veilix.app, dev.veilixvpn.com, dev.my.veilixvpn.com | 127.0.0.1:3100 | только разрешённые IP или пароль; `X-Robots-Tag: noindex` |
|
||||
| dev.tg.veilix.app, dev.tg.veilixvpn.com | 127.0.0.1:3100 | открыт для Telegram; приложение пускает только разрешённые Telegram-аккаунты |
|
||||
| dev.plc.veilix.cc | 127.0.0.1:3100 | только разрешённые IP |
|
||||
| umi.veilix.cc | 127.0.0.1:3300 | только разрешённые IP |
|
||||
|
||||
1. Слушает 169.58.76.183, порты 80 (редирект на HTTPS и проверка certbot) и 443. TLS 1.2 и 1.3, HTTP/2, HSTS.
|
||||
2. Сертификаты — certbot, проверка через HTTP.
|
||||
3. Сжатие — brotli и gzip.
|
||||
4. GeoIP: блок `geoip2` в `/etc/nginx/conf/ip.conf`, база `/var/lib/GeoIP/GeoLite2-Country.mmdb` с `auto_reload`. Переменная `$geoip_country_code` уходит приложению заголовком `X-Geo-Country`; такой же заголовок от клиента nginx затирает. Старый модуль GeoIP v1 (`modules-enabled/50-mod-http-geoip.conf`) отключён: он занимает те же имена переменных, и `nginx -t` падает с ошибкой «the duplicate "geoip_country_code" variable».
|
||||
5. Заголовки к приложению — общий файл `/etc/nginx/snippets/veilix_proxy.conf` (плюс `maps/upgrade.conf` для WebSocket), его подключает каждый `location` с `proxy_pass`: `Host`, `X-Real-IP` и `X-Forwarded-For` (оба — `$remote_addr`: nginx стоит первым, цепочку от клиента не принимаем), `X-Forwarded-Proto`, `X-Geo-Country`, заголовки WebSocket (нужны горячей перезагрузке dev). `proxy_set_header` не наследуется в `location` со своими `proxy_set_header`, поэтому файл подключается в каждый такой `location`.
|
||||
6. В `nginx.conf` подключаются только `sites-enabled/*.conf`: файлы сайтов должны заканчиваться на `.conf`. Сайты: `sites-available/02_vx_veilix-prod.conf`, `02_vx_veilix-dev.conf`, `02_vx_plc-prod.conf`, `02_vx_plc-dev.conf`. Сертификаты Let's Encrypt (certbot, проверка через nginx): `veilix.app` (6 prod-хостов брендов), `dev.veilix.app` (6 dev-хостов), `plc.veilix.cc`, `dev.plc.veilix.cc`.
|
||||
a. Доступ по IP — файл `conf/ip_list_admin.conf` со строками `allow …;`, в `location` после него `deny all;`. Подключается на хостах админки и dev, кроме `dev.tg.*`.
|
||||
b. `dev.tg.*` — отдельный `server` без ограничения по IP, с `X-Robots-Tag: noindex`.
|
||||
c. На порту 80 — `location / { return 301 https://$host$request_uri; }`. Именно внутри `location`, а не на уровне `server`: так certbot при продлении может вставить свой `location` для проверки домена.
|
||||
7. Лимиты запросов (`limit_req`) на эндпоинты входа и формы.
|
||||
8. Для tg.* (этап 3) разрешить показ во фрейме Telegram Web (`frame-ancestors` для доменов telegram.org).
|
||||
9. Пути трекера Umami на доменах брендов проксируются на 127.0.0.1:3300 ([08-integrations.md](08-integrations.md), раздел 8).
|
||||
10. Конфигурацию готовит Claude текстом на согласование, применяет root.
|
||||
|
||||
## 5. PostgreSQL
|
||||
|
||||
1. Базы `veilix_dev`, `veilix_prod` и `umami`. Владелец каждой — роль с именем пользователя ОС: `vx_dev`, `vx_prod`, `umami`.
|
||||
2. Вход по Unix-сокету с проверкой пользователя ОС: стандартное правило Ubuntu `local all all peer` в `pg_hba.conf` пускает пользователя ОС только под ролью с тем же именем. Паролей нет, `pg_hba.conf` и `pg_ident.conf` править не нужно.
|
||||
3. У каждой базы снято право подключения для всех (`REVOKE CONNECT … FROM PUBLIC`): войти может только владелец. vx_dev в `veilix_prod` не попадёт.
|
||||
4. Роль владеет своей базой: Payload нужны права на изменение схемы (push в dev, миграции в prod).
|
||||
5. Строка подключения: `postgresql://vx_dev@/veilix_dev?host=/var/run/postgresql` (для prod — `vx_prod@/veilix_prod`).
|
||||
|
||||
## 6. Почта (Postfix)
|
||||
|
||||
1. `smtp_bind_address = 169.58.76.183`, `myhostname = fr-ub2gs3.veilix.cc`: письма уходят с IP, у которого обратная запись совпадает с прямой.
|
||||
2. OpenDKIM: ключи для veilix.app и veilixvpn.com (селектор, напр. `mail`).
|
||||
3. DNS для каждого из двух доменов:
|
||||
a. SPF: `v=spf1 ip4:169.58.76.183 -all` (плюс SMTP-провайдер, если используем);
|
||||
b. DKIM: TXT `mail._domainkey.<домен>` с открытым ключом;
|
||||
c. DMARC: `_dmarc.<домен>` — сначала `v=DMARC1; p=none; rua=mailto:…`, после проверки — `p=quarantine`.
|
||||
4. Проверить, что хостер не закрывает исходящий порт 25 и что 169.58.76.183 нет в чёрных списках.
|
||||
5. Приём почты поддержки (`help@…`) — где ящик, вопрос Q-16.
|
||||
|
||||
## 7. Бэкапы
|
||||
|
||||
1. Prod-база: каждую ночь `pg_dump -Fc veilix_prod`, хранить 14 ежедневных и 8 еженедельных, копия на другой сервер (куда — Q-14). Раз в месяц — проверочное восстановление в отдельную базу.
|
||||
2. `shared/media` — синхронизация на тот же внешний сервер.
|
||||
3. Dev не бэкапим: код — в git, данные dev — тестовые.
|
||||
|
||||
## 8. Задачи для root по этапам
|
||||
|
||||
| № | Этап | Задача | Статус на 21.10.2026 |
|
||||
|---|---|---|---|
|
||||
| R-00 | 0 | Переезд разработки: перенести `/home/vx/veilix_dev` в `/home/vx_dev/veilix_dev` (владелец vx_dev); SSH-ключ vx_dev для VSCode; Claude Code под vx_dev | не сделано |
|
||||
| R-01 | 0 | Репозиторий в Gitea; SSH-ключ vx_dev с правом push; для vx_prod — ключ только на чтение (deploy key) | не сделано |
|
||||
| R-02 | 1a | PostgreSQL: роли `vx_dev`, `vx_prod`, `umami` и их базы (раздел 5) | сделано и проверено: vx_dev входит в `veilix_dev`, в `veilix_prod` — отказ |
|
||||
| R-03 | 1a | pnpm: `corepack enable pnpm` | сделано |
|
||||
| R-04 | 1a | Пользователи vx_dev, vx_prod, umami; `loginctl enable-linger` для них | сделано |
|
||||
| R-05 | 1a | nginx и сертификаты для всех хостов (раздел 4) | работает; осталось исправить опечатку в `conf/ip_list_admin.conf` и добавить `X-Robots-Tag` для `dev.tg.*` |
|
||||
| R-06 | 1a | MaxMind: `geoipupdate` (ключ в `/etc/GeoIP.conf`, база в `/var/lib/GeoIP`, таймер `geoipupdate.timer`); отключить старый модуль GeoIP v1; блок `geoip2` (раздел 4, пункт 4) | сделано |
|
||||
| R-07 | 1c | Postfix, OpenDKIM, DNS-записи SPF, DKIM, DMARC — или данные SMTP-провайдера | |
|
||||
| R-08 | 1h | PostgreSQL для prod — если не сделано вместе с R-02 | сделано в R-02 |
|
||||
| R-09 | 1h | nginx и сертификаты для prod-хостов | сделано вместе с R-05 |
|
||||
| R-10 | 1h | Umami: nginx и сертификат для `umi.veilix.cc` (DNS уже есть; база — в R-02) | |
|
||||
| R-11 | 1h | Бэкапы prod-базы и media на внешний сервер (Q-14) | |
|
||||
| R-12 | 2 | VPS с Chatwoot и Twenty, ключи API; 2 бота поддержки в BotFather | |
|
||||
| R-13 | 3 | Боты в BotFather: 2 основных и тестовые; домен для Telegram Login; адреса MiniApp | |
|
||||
| R-14 | 4 | Аккаунты платёжных систем (сначала тестовые), адреса вебхуков | |
|
||||
| R-15 | 1h | По желанию (Q-18): sudo-правило, разрешающее vx_dev запускать от имени vx_prod только команду выкладки | |
|
||||
| R-16 | 1h | По желанию: правило nftables — к 127.0.0.1:3000 подключаются только `www-data` (nginx) и vx_prod ([10-security.md](10-security.md), раздел 7) | |
|
||||
@@ -0,0 +1,56 @@
|
||||
# 10. Безопасность и приватность
|
||||
|
||||
## 1. Идентификаторы
|
||||
|
||||
1. Все `id` — UUID. В URL, API и консоли браузера нет последовательных номеров.
|
||||
2. Номера заказов, ключей, платежей и реферальные коды — случайные строки (алфавит Crockford base32, без похожих символов).
|
||||
|
||||
## 2. Секреты
|
||||
|
||||
1. Секреты — только в `.env` своего окружения, права 600, вне git: строка подключения к базе, секрет сессий и кодов, ключ шифрования полей, ключ ALTCHA, ключи платёжных систем, токены ботов.
|
||||
2. Чувствительные поля в базе шифруются приложением (AES-256-GCM): содержимое подключений (конфиги WG, ссылки подписок), SMTP-пароль, id сохранённых карт у провайдера.
|
||||
3. Для prod ключи свои, не такие, как в dev.
|
||||
|
||||
## 3. Админка
|
||||
|
||||
1. Только на `plc.veilix.cc` и `dev.plc.veilix.cc`, вход только с разрешённых IP (nginx).
|
||||
2. Роли owner, admin, support, content ([03-data-model.md](03-data-model.md), раздел 10). Поле `debugCode` журнала писем видят только owner и admin.
|
||||
3. Блокировка после 5 неудачных попыток входа.
|
||||
4. Денежные операции, выдача и блокировка ключей, объединение аккаунтов пишутся в журнал действий. Журнал нельзя изменить.
|
||||
|
||||
## 4. Пользователи
|
||||
|
||||
1. Коды входа хранятся только как HMAC, проверяются за постоянное время, 5 попыток, 10 минут жизни.
|
||||
2. ALTCHA и лимиты по IP и email на запрос кода.
|
||||
3. Ответ на запрос кода не выдаёт, есть ли аккаунт.
|
||||
4. Токены сессий в базе — только SHA-256. Cookie — `HttpOnly`, `Secure`, `SameSite=Lax`, привязаны к хосту.
|
||||
5. CSRF: `SameSite=Lax`, проверка Origin в server actions и своих POST-эндпоинтах.
|
||||
6. Доступ к данным — правила Payload: пользователь видит только свои ключи, заказы, платежи и движения. Кабинет ходит в Local API с `overrideAccess: false`.
|
||||
7. Коды входа в журнале ([05-auth.md](05-auth.md), раздел 4) — только для тестирования. В prod выключено, включение видно в админке.
|
||||
|
||||
## 5. Платежи
|
||||
|
||||
1. Номер карты никогда не попадает на наши серверы: только виджеты и страницы провайдеров.
|
||||
2. Вебхуки проверяются по подписи провайдера, повторная доставка не проводит платёж дважды.
|
||||
3. Деньги — целые числа, операции в транзакциях с блокировками ([04-billing.md](04-billing.md), раздел 14).
|
||||
|
||||
## 6. Приватность
|
||||
|
||||
1. Храним минимум: email, Telegram, язык, валюту, история заказов и платежей.
|
||||
2. IP пользователей в базе не храним в открытом виде — только HMAC для сравнения (самоприглашения, лимиты). Логи nginx — ротация 14 дней.
|
||||
3. В списке приглашённых почта замаскирована: `m···@gmail.com`.
|
||||
4. Аналитика — Umami без cookies, данные не уходят третьим лицам.
|
||||
5. Что и зачем храним — в политике конфиденциальности (этап 1b).
|
||||
|
||||
## 7. Окружения
|
||||
|
||||
1. Dev и prod — разные пользователи ОС (vx_dev и vx_prod), базы, роли и секреты. Dev закрыт от посторонних и от индексации.
|
||||
2. Сессии Claude и редактор работают под vx_dev и не имеют доступа к файлам, секретам, логам и базе prod. Под vx_prod нет ни редактора, ни ИИ-агентов, VSCode Remote под vx_prod не подключаем. Своего SSH-входа у vx_prod нет: под него заходят из root (`sudo -iu vx_prod`). Код попадает в prod только через тег в Gitea и команду выкладки ([02-architecture.md](02-architecture.md), раздел 5).
|
||||
3. Umami работает под своим пользователем: уязвимость в нём не открывает доступ к секретам Veilix.
|
||||
4. В базу каждый пользователь ОС входит только своей ролью (`peer` через Unix-сокет), паролей нет ([09-infrastructure.md](09-infrastructure.md), раздел 5).
|
||||
5. Порты приложений 3000 и 3100 слушают 127.0.0.1, к ним может подключиться любой пользователь сервера в обход ограничения по IP в nginx. Секретов это не открывает: админку защищает пароль с блокировкой после 5 попыток. Строже — правило nftables, которое пускает к порту prod только nginx (`www-data`) и vx_prod (R-16).
|
||||
|
||||
## 8. Обновления
|
||||
|
||||
1. Payload и Next.js выпускают исправления безопасности часто: проверять при каждом этапе, обновлять патч-версии.
|
||||
2. `pnpm audit` перед выкладкой.
|
||||
@@ -0,0 +1,24 @@
|
||||
# D-001. Одно Next.js-приложение на все домены
|
||||
|
||||
Статус: принято, 20.10.2026.
|
||||
|
||||
## Контекст
|
||||
|
||||
Два бренда, у каждого сайт, кабинет и MiniApp — 6 доменов плюс админка. Бизнес-логика общая, дизайны разные.
|
||||
|
||||
## Решение
|
||||
|
||||
1. Одно Next.js-приложение, Payload установлен в него как библиотека.
|
||||
2. Proxy Next.js определяет по Host сайт и тип фронтенда и переписывает запрос во внутренний маршрут.
|
||||
3. Ядро (биллинг, ключи, вход, рефералы, почта) и UI-кит общие, страницы у каждого бренда свои.
|
||||
|
||||
## Рассмотренные варианты
|
||||
|
||||
1. Отдельные приложения на каждый фронтенд и отдельный Payload как API — больше деплоев, логика дублируется или ходит по HTTP.
|
||||
2. Один набор страниц с переключением цветов — не подходит: данные в дизайнах представлены по-разному.
|
||||
|
||||
## Последствия
|
||||
|
||||
1. Один процесс на окружение, один деплой.
|
||||
2. Кабинет и сайт читают данные через Local API Payload, без HTTP.
|
||||
3. Нужна дисциплина: бренд-зависимый код — только в `src/brands/`.
|
||||
@@ -0,0 +1,21 @@
|
||||
# D-002. Payload 3, переход на 4 после стабильного релиза
|
||||
|
||||
Статус: принято, 20.10.2026.
|
||||
|
||||
## Контекст
|
||||
|
||||
На 20.10.2026 стабильная версия — Payload 3.91.0. Payload 4 в бете (4.0.0-beta.2 от 16.10.2026, требует Node ≥ 24.15 и Next.js ≥ 16.4). У Payload есть официальный codemod для перехода с 3 на 4.
|
||||
|
||||
## Решение
|
||||
|
||||
1. Стартуем на последней 3.x и Next.js 16.4.x.
|
||||
2. Код пишем совместимо с 4: в Local API всегда явно `overrideAccess`, rich text — только Lexical, без устаревших API.
|
||||
3. Переходим на 4 после стабильного релиза. Если он выйдет до старта этапа 1a — начинаем сразу на 4.
|
||||
|
||||
## Рассмотренные варианты
|
||||
|
||||
Начать на бете 4 — нет переезда потом, но риск ошибок и меняющихся API в бете, которой неделя.
|
||||
|
||||
## Последствия
|
||||
|
||||
Один плановый переезд, пока данных в prod мало или нет.
|
||||
@@ -0,0 +1,22 @@
|
||||
# D-003. Без Docker: всё на хосте, процессы под systemd
|
||||
|
||||
Статус: принято, 20.10.2026.
|
||||
|
||||
## Контекст
|
||||
|
||||
На сервере уже работают PostgreSQL 18, nginx и Redis. Пользователь хочет, чтобы dev и prod запускались одинаково и без Docker.
|
||||
|
||||
## Решение
|
||||
|
||||
1. PostgreSQL и nginx — на хосте.
|
||||
2. Приложение — обычный процесс Node: dev — `pnpm dev`, prod — `pnpm build` и `pnpm start`. Оба — systemd user-юниты: dev у пользователя vx_dev, prod у vx_prod ([D-004](D-004-environments.md)).
|
||||
3. Prod выкладывается в отдельную папку релиза и переключается симлинком `current`. Откат — переключить симлинк обратно.
|
||||
|
||||
## Рассмотренные варианты
|
||||
|
||||
Docker-образ для prod — воспроизводимая сборка и изоляция, но лишний слой. Тот же откат даёт схема с папками релизов.
|
||||
|
||||
## Последствия
|
||||
|
||||
1. Сборка prod идёт на этом же сервере (нагрузка 2–4 ГБ памяти на время сборки).
|
||||
2. Node и pnpm общие для dev и prod: обновляются вместе.
|
||||
@@ -0,0 +1,41 @@
|
||||
# D-004. Dev и prod на одном сервере под разными пользователями
|
||||
|
||||
Статус: принято, 21.10.2026.
|
||||
|
||||
## Контекст
|
||||
|
||||
Dev и prod живут на одном сервере без Docker. Редактор и сессии Claude работают в dev и не должны иметь доступа к prod.
|
||||
|
||||
## Решение
|
||||
|
||||
| | dev | prod |
|
||||
|---|---|---|
|
||||
| Пользователь ОС | vx_dev | vx_prod |
|
||||
| Папка | `/home/vx_dev/veilix_dev` | `/home/vx_prod/veilix` |
|
||||
| База и роль | `veilix_dev`, роль `vx_dev` | `veilix_prod`, роль `vx_prod` |
|
||||
| Порт | 3100 | 3000 |
|
||||
| Домены | `dev.veilix.app`, `dev.my.veilix.app`, `dev.tg.veilix.app`, так же для veilixvpn.com; `dev.plc.veilix.cc` | основные; `plc.veilix.cc` |
|
||||
|
||||
1. vx — личный пользователь администратора, к проекту отношения не имеет.
|
||||
2. Вся разработка — под vx_dev: VSCode (Remote-SSH), Claude Code и другие ИИ-агенты запускаются только под этим пользователем.
|
||||
3. Под vx_prod — только prod:
|
||||
a. редактора и ИИ-агентов нет, VSCode Remote под vx_prod не подключаем: при подключении VSCode ставит на сервер свою серверную часть и может подтянуть туда расширения, в том числе ИИ-агентов;
|
||||
b. своего SSH-входа нет, под vx_prod заходят из root (`sudo -iu vx_prod`).
|
||||
4. Оба окружения запускаются одинаково — systemd user-юнитами, каждый у своего пользователя.
|
||||
5. Домашние папки — `750`, владелец — сам пользователь: секреты, файлы и логи prod сессиям Claude недоступны.
|
||||
6. В базу каждый пользователь ОС входит только своей ролью (`peer` через Unix-сокет), паролей нет; чужие базы закрыты `REVOKE CONNECT … FROM PUBLIC`.
|
||||
7. Код попадает в prod только через Gitea: из dev публикуется тег, vx_prod забирает его ключом только на чтение и собирает релиз.
|
||||
8. Выкладку запускает пользователь под vx_prod. По желанию — sudo-правило, разрешающее vx_dev запускать от имени vx_prod только команду выкладки (Q-18).
|
||||
9. Домены хранятся в базе, поэтому у dev просто свои записи в своей базе.
|
||||
|
||||
## Рассмотренные варианты
|
||||
|
||||
1. Один пользователь для обоих окружений — проще, но сессии Claude и любой процесс dev технически достают до секретов и базы prod.
|
||||
2. Prod под личным пользователем vx — под ним работают редактор и ИИ-агенты, защита держалась бы только на дисциплине.
|
||||
3. Docker-контейнеры для изоляции — пользователь не хочет Docker.
|
||||
|
||||
## Последствия
|
||||
|
||||
1. Ошибки prod сессии Claude не видят напрямую: логи присылает пользователь.
|
||||
2. Отдельные deny-правила Claude Code для prod не нужны.
|
||||
3. Переезд разработки на vx_dev — задача R-00 ([09-infrastructure.md](../09-infrastructure.md), раздел 8).
|
||||
@@ -0,0 +1,18 @@
|
||||
# D-005. Сайты — тенанты, домены в базе, маршрутизация по Host
|
||||
|
||||
Статус: принято, 20.10.2026.
|
||||
|
||||
## Решение
|
||||
|
||||
1. Официальный плагин `@payloadcms/plugin-multi-tenant`: тенант — Сайт, поле `site` на коллекциях, привязанных к сайту, переключатель сайта в админке.
|
||||
2. Коллекция «Домены»: хост, сайт, тип фронтенда, языки, валюты.
|
||||
3. Proxy Next.js находит домен по Host (с кэшем в памяти) и переписывает запрос во внутренний маршрут.
|
||||
4. Общие справочники без привязки к сайту: страны, теги, валюты, платёжные системы. Платёжная система привязана к валютам, а сайт выводится через валюты его доменов ([D-010](D-010-currencies.md)).
|
||||
|
||||
## Рассмотренные варианты
|
||||
|
||||
Простое поле «сайт» без плагина — придётся самим писать фильтры и доступ в админке.
|
||||
|
||||
## Последствия
|
||||
|
||||
Определение сайта по домену плагин не делает — это наш код в proxy.
|
||||
@@ -0,0 +1,20 @@
|
||||
# D-006. Переводы: поля Payload и next-intl; 5 языков
|
||||
|
||||
Статус: принято, 20.10.2026.
|
||||
|
||||
## Решение
|
||||
|
||||
1. Языки: `en` (по умолчанию, без префикса в URL), `ru`, `de`, `fr`, `es`. Позже — `pt-br` и `ja`.
|
||||
2. Контент — локализованные поля Payload (`localized: true`), откат на английский.
|
||||
3. Интерфейс — next-intl, файлы `messages/*.json` в репозитории.
|
||||
4. Язык всегда в URL (кроме английского). Список языков — в конфиге, у домена — включённые языки. Отдельной сущности «Языки» нет.
|
||||
5. В админке Payload `filterAvailableLocales` скрывает языки, выключенные для сайта.
|
||||
|
||||
## Рассмотренные варианты
|
||||
|
||||
1. Языки как записи в базе — Payload задаёт локали в конфиге, сущность дублировала бы конфиг.
|
||||
2. Интерфейсные строки тоже в Payload — нет версионирования и типизации, лишние запросы.
|
||||
|
||||
## Последствия
|
||||
|
||||
Новый язык — правка конфига, переводы и деплой.
|
||||
@@ -0,0 +1,17 @@
|
||||
# D-007. Определение языка; MaxMind через nginx
|
||||
|
||||
Статус: принято, 20.10.2026.
|
||||
|
||||
## Решение
|
||||
|
||||
1. Порядок: язык в URL → сохранённый выбор (cookie, профиль) → `Accept-Language` → страна по MaxMind → `en`.
|
||||
2. Перенаправление — только при первом заходе без префикса и только людей, не поисковых ботов. Плюс `hreflang` и `x-default`.
|
||||
3. Страну определяет nginx (модуль geoip2, база GeoLite2-Country) и передаёт заголовком `X-Geo-Country`. Приложение само читает `.mmdb` только там, где нужна страна произвольного IP.
|
||||
|
||||
## Рассмотренные варианты
|
||||
|
||||
Язык только по стране — страна не равна языку, а Google не рекомендует перенаправлять по IP.
|
||||
|
||||
## Последствия
|
||||
|
||||
База MaxMind обновляется `geoipupdate` (root).
|
||||
@@ -0,0 +1,16 @@
|
||||
# D-008. UUID и случайные публичные коды
|
||||
|
||||
Статус: принято, 20.10.2026.
|
||||
|
||||
## Решение
|
||||
|
||||
1. `idType: 'uuid'` в адаптере Postgres Payload: все id в базе, REST и URL админки — UUID. Автоинкремента нет.
|
||||
2. Номера для людей — короткие случайные коды: заказы `VX-7K3M9Q`, ключи `K-…`, платежи, реферальные коды.
|
||||
|
||||
## Рассмотренные варианты
|
||||
|
||||
Автоинкремент плюс отдельное поле UUID для внешнего мира — нужно следить, чтобы внутренний id нигде не утёк. Штатная настройка Payload решает задачу без кастома.
|
||||
|
||||
## Последствия
|
||||
|
||||
Уникальность случайных кодов обеспечивается уникальным индексом и повтором генерации при коллизии.
|
||||
@@ -0,0 +1,24 @@
|
||||
# D-009. Заказы, платежи, движения по счёту; всё через баланс
|
||||
|
||||
Статус: принято, 20.10.2026.
|
||||
|
||||
## Контекст
|
||||
|
||||
У пользователя есть счёт. Заказ можно оплатить картой, с баланса или частично. Нужна ручная оплата из админки, которая запускает те же действия, что и настоящая.
|
||||
|
||||
## Решение
|
||||
|
||||
1. Три сущности: Заказ (что покупают), Платёж (деньги извне), Движение по счёту (неизменяемые записи «+» и «−»).
|
||||
2. Любой успешный платёж зачисляется на баланс, любой заказ списывается с баланса.
|
||||
3. Одна точка проведения `processPayment`, идемпотентная.
|
||||
4. Ручная оплата — платёжная система `manual`. Платёж проводится, когда получает статус `succeeded`. Галочки «провести как реальную» нет.
|
||||
5. Корректировка баланса — отдельная операция: только движение, без цепочки действий.
|
||||
|
||||
## Рассмотренные варианты
|
||||
|
||||
1. Одна сущность «транзакция» — смешивает коммерческий документ, деньги провайдера и баланс; сложно с частичной оплатой и повторами вебхуков.
|
||||
2. Галочка «провести как реальную оплату» — не нужна: проведение запускает смена статуса.
|
||||
|
||||
## Последствия
|
||||
|
||||
Правило «списывается только разница с балансом» получается само. Подробно — [04-billing.md](../04-billing.md).
|
||||
@@ -0,0 +1,22 @@
|
||||
# D-010. Валюты и платёжные системы — сущности; платёжные системы привязаны к валютам
|
||||
|
||||
Статус: принято, 20.10.2026.
|
||||
|
||||
## Контекст
|
||||
|
||||
Из России часто заходят через VPN, поэтому страна по IP не говорит, какая у пользователя валюта и какие карты. Карты РФ принимают одни платёжные системы, зарубежные — другие.
|
||||
|
||||
## Решение
|
||||
|
||||
1. Сущность «Валюты»: RUB и USD.
|
||||
2. Сущность «Платёжные системы»: адаптер, валюты, назначения (оплата заказа, пополнение). RUB — процессинги для карт РФ (вероятно, KZ и TR), USD — Stripe и криптовалюта.
|
||||
3. Платёжная система привязана только к валютам. Где она доступна, следует из цепочки: валюты → домены, на которых они включены → сайты. В оформлении заказа показываются системы в валюте аккаунта.
|
||||
4. У домена — включённые валюты, валюта по умолчанию и валюта для языка: ru → RUB, остальные → USD.
|
||||
5. Переключатель валюты отдельный от языка: любая валюта с любым языком.
|
||||
6. Цены тарифов — строкой на каждую валюту.
|
||||
7. Валюта аккаунта задаётся при регистрации из выбранной и меняется, пока баланс нулевой и нет ключей. Дальше — через поддержку.
|
||||
|
||||
## Последствия
|
||||
|
||||
1. Маркетинговые страницы собираются в вариантах «язык × валюта» ([D-017](D-017-static-pages.md)).
|
||||
2. Платёжная система в RUB доступна сразу на обоих брендах. Если понадобятся разные аккаунты одного провайдера для разных брендов, платёжной системе добавим необязательное поле «только для сайтов» (Q-17).
|
||||
@@ -0,0 +1,17 @@
|
||||
# D-011. Раздельные аккаунты по сайтам
|
||||
|
||||
Статус: принято, 20.10.2026.
|
||||
|
||||
## Решение
|
||||
|
||||
1. Пользователь привязан к сайту. Один email или Telegram на двух сайтах — два разных пользователя.
|
||||
2. Внутри сайта: аккаунт можно создать по Telegram, почта привязывается кодом; на сайте есть «Войти через Telegram»; аккаунты объединяются по правилам [05-auth.md](../05-auth.md), раздел 6.4.
|
||||
|
||||
## Рассмотренные варианты
|
||||
|
||||
1. Общий аккаунт на оба бренда — бренды смешиваются в кабинете и в балансе.
|
||||
2. Общий вход и отдельный профиль в каждом бренде — без дублей, но сложнее модель. Пользователь выбрал простоту.
|
||||
|
||||
## Последствия
|
||||
|
||||
Переход клиента с mass на dedicated — новая регистрация на veilix.app.
|
||||
@@ -0,0 +1,18 @@
|
||||
# D-012. Вход по коду через свою стратегию Payload; оформление заказа на my.*
|
||||
|
||||
Статус: принято, 20.10.2026.
|
||||
|
||||
## Решение
|
||||
|
||||
1. Входа по коду в Payload из коробки нет. Используем штатный механизм своих стратегий: коллекция `customers` с `disableLocalStrategy` и стратегией, которая читает нашу cookie сессии.
|
||||
2. Коды и сессии — свои коллекции; коды и токены хранятся только как хэши.
|
||||
3. Оформление заказа живёт на домене кабинета (my.*), а не на маркетинговом. Cookie сессии — только для хоста кабинета.
|
||||
|
||||
## Рассмотренные варианты
|
||||
|
||||
1. Better Auth или Auth.js поверх Payload — ещё одна система со своими таблицами и миграциями.
|
||||
2. Cookie на весь домен бренда (`.veilix.app`) — тогда маркетинговые страницы перестают быть полностью кэшируемыми, а cookie видят dev-поддомены.
|
||||
|
||||
## Последствия
|
||||
|
||||
Маркетинговые сайты остаются без пользовательских cookies и полностью кэшируются.
|
||||
@@ -0,0 +1,16 @@
|
||||
# D-013. Капча ALTCHA
|
||||
|
||||
Статус: принято, 20.10.2026.
|
||||
|
||||
## Решение
|
||||
|
||||
ALTCHA — капча на proof-of-work, работает на нашем сервере, без третьих сторон. Вместе с лимитами по IP и email.
|
||||
|
||||
## Рассмотренные варианты
|
||||
|
||||
1. Google reCAPTCHA — передаёт данные пользователей в Google и строже проверяет пользователей VPN, то есть нашу аудиторию.
|
||||
2. Cloudflare Turnstile — с июня 2025 российские провайдеры замедляют Cloudflare, пользователи из РФ могут не пройти проверку.
|
||||
|
||||
## Последствия
|
||||
|
||||
Провайдер капчи изолирован в `core/` и заменяется без переделки форм.
|
||||
@@ -0,0 +1,18 @@
|
||||
# D-014. Почта: шаблоны в Payload, переключаемый транспорт, без listmonk
|
||||
|
||||
Статус: принято, 20.10.2026.
|
||||
|
||||
## Решение
|
||||
|
||||
1. Шаблоны писем — в Payload, переведены, для каждого сайта свои.
|
||||
2. Транспорт переключается в админке: SMTP, локальный sendmail (Postfix) или «только журнал».
|
||||
3. Все письма пишутся в журнал. Флаг «Записывать коды входа в журнал» — для тестирования.
|
||||
4. listmonk не используем.
|
||||
|
||||
## Рассмотренные варианты
|
||||
|
||||
listmonk — прежде всего сервис рассылок; переводов шаблонов в нём нет: на каждое письмо понадобилось бы 10 шаблонов (2 бренда × 5 языков).
|
||||
|
||||
## Последствия
|
||||
|
||||
Для отправки через Postfix нужны SPF, DKIM, DMARC и отправка с IP с правильной обратной записью ([09-infrastructure.md](../09-infrastructure.md), раздел 6).
|
||||
@@ -0,0 +1,19 @@
|
||||
# D-015. Боты на grammY без n8n; два бота на бренд
|
||||
|
||||
Статус: принято, 20.10.2026.
|
||||
|
||||
## Решение
|
||||
|
||||
1. Основной бот каждого бренда (команды, кнопка MiniApp, уведомления) написан в коде на grammY и работает внутри нашего приложения через вебхук. Тексты и кнопки — в Payload, переведены, правятся в админке.
|
||||
2. Бот поддержки каждого бренда подключён к Chatwoot напрямую.
|
||||
3. MiniApp — фронтенд в том же приложении, без n8n.
|
||||
4. n8n не используем.
|
||||
|
||||
## Рассмотренные варианты
|
||||
|
||||
1. n8n как движок бота — визуальные схемы для покупок, ключей и баланса на 5 языках × 2 бота трудно поддерживать и тестировать, логика денег всё равно живёт в бэкенде.
|
||||
2. Один бот на всё с пересылкой сообщений поддержки в Chatwoot через API — свой код, который нужно поддерживать.
|
||||
|
||||
## Последствия
|
||||
|
||||
Telegram отдаёт обновления бота только одному получателю, поэтому основной бот и бот поддержки — разные боты.
|
||||
@@ -0,0 +1,15 @@
|
||||
# D-016. Платёжные системы по валютам; автосписания — Stripe
|
||||
|
||||
Статус: принято, 20.10.2026. Провайдеры для RUB и криптовалюты выбираются на этапе 4 (Q-12).
|
||||
|
||||
## Решение
|
||||
|
||||
1. Каждый провайдер — адаптер с единым интерфейсом и флагами возможностей: автосписание, возврат, сохранение карты.
|
||||
2. USD — Stripe (карты, сохранение карты, автосписания на переменную сумму, без Stripe Subscriptions) и криптовалюта.
|
||||
3. RUB — процессинги, принимающие карты РФ (вероятно, KZ и TR), только пополнения, без автосписаний. ЮKassa — опционально, только пополнения.
|
||||
4. Пользователи с картами РФ продлевают ключи с баланса; напоминание — за 3 дня.
|
||||
5. Оплата внутри Telegram (Stars) — решаем на этапе 3.
|
||||
|
||||
## Последствия
|
||||
|
||||
На этапе 1 работает только `manual`. Модель данных готова к любому набору провайдеров.
|
||||
@@ -0,0 +1,14 @@
|
||||
# D-017. Статические маркетинговые страницы с пересборкой по событию
|
||||
|
||||
Статус: принято, 20.10.2026.
|
||||
|
||||
## Решение
|
||||
|
||||
1. Маркетинговые страницы собираются статически в вариантах «язык × валюта» и кэшируются.
|
||||
2. Изменение в админке (цены, статус локации, FAQ, страницы) пересобирает затронутые страницы за секунды через пересборку по тегам. Запасной вариант — плановое обновление раз в 5 минут.
|
||||
3. Наличие и цена перепроверяются на сервере при оформлении заказа.
|
||||
4. Кабинет рендерится на сервере для каждого запроса.
|
||||
|
||||
## Последствия
|
||||
|
||||
Высокий балл PageSpeed, а устаревшая страница не может продать недоступную локацию.
|
||||
@@ -0,0 +1,20 @@
|
||||
# D-018. Umami на этом сервере и баннер cookies
|
||||
|
||||
Статус: принято, 20.10.2026.
|
||||
|
||||
## Решение
|
||||
|
||||
1. Аналитика — своя Umami без cookies, на этом сервере: Node.js и PostgreSQL здесь уже есть, а трекер проксирует этот же nginx.
|
||||
2. Ставится из исходников, без Docker (официальный способ наряду с Docker-образом): `pnpm install`, `pnpm build`, `pnpm start`. Отдельный пользователь ОС `umami`, своя база `umami`, systemd user-юнит, порт 3300.
|
||||
3. Панель — `umi.veilix.cc`, доступ только с разрешённых IP.
|
||||
4. Скрипт и отправка событий проксируются через домены брендов под нейтральными путями (`TRACKER_SCRIPT_NAME`, `COLLECT_API_ENDPOINT`), чтобы их не резали блокировщики рекламы.
|
||||
5. Баннер cookies: обязательные (сессия, язык, валюта) и функциональные (чат). Необязательные скрипты — только после согласия.
|
||||
|
||||
## Рассмотренные варианты
|
||||
|
||||
1. Umami на VPS с Chatwoot и Twenty — трекеру пришлось бы ходить на другой сервер, где Docker и тяжёлые сервисы.
|
||||
2. Яндекс Метрика и GA4 — передают данные третьим лицам и требуют согласия; нужны, только если будет реклама.
|
||||
|
||||
## Последствия
|
||||
|
||||
У Umami свой пользователь ОС, поэтому уязвимость в ней не открывает доступ к секретам Veilix.
|
||||
@@ -0,0 +1,13 @@
|
||||
# D-019. Chatwoot и Twenty CRM на отдельном VPS
|
||||
|
||||
Статус: принято, 20.10.2026.
|
||||
|
||||
## Решение
|
||||
|
||||
1. Chatwoot — один экземпляр на оба бренда: виджеты сайтов и боты поддержки как отдельные входящие каналы.
|
||||
2. Twenty CRM — заявки из Payload уходят в Twenty через API.
|
||||
3. Оба сервиса на отдельном VPS, ставит и обслуживает пользователь. Наше приложение работает с ними только по API.
|
||||
|
||||
## Последствия
|
||||
|
||||
До этапа 2 заявки видны в админке Payload и приходят письмом на почту поддержки.
|
||||
@@ -0,0 +1,16 @@
|
||||
# D-020. Модели ключей dedicated и mass
|
||||
|
||||
Статус: принято, 20.10.2026.
|
||||
|
||||
## Решение
|
||||
|
||||
1. Dedicated: ключ — индивидуальный IP в стране, выбранной при покупке. Страна не меняется никогда: нужна другая — покупается новый ключ. В ключ входит RU-адрес. Подключения: WG/AWG-конфиг на каждое устройство (до лимита тарифа), ссылка Happ или Xray, подключения к RU-адресу.
|
||||
2. Mass: ключ включает все страны тарифа, страна не выбирается. Happ — одна ссылка со всеми странами. WG — конфиги любой страны тарифа: у подключения есть поле страны.
|
||||
3. Тип ключа определяется полем `type` тарифа.
|
||||
4. Подключения гибкие: у ключа любой набор подключений разных видов, поэтому ручная выдача на этапе 1 покрывает любой вариант.
|
||||
5. Оплаченный период начинается с момента выдачи ключа.
|
||||
6. На этапе 1 WG-конфиги mass — те, что добавил администратор, по странам. Нужна другая страна — через поддержку. Автоматически — на этапе 5.
|
||||
|
||||
## Последствия
|
||||
|
||||
Автоматическая выдача (этап 5) создаёт те же подключения, что сейчас добавляет администратор.
|
||||
@@ -0,0 +1,15 @@
|
||||
# D-021. Админка на plc.veilix.cc с доступом по IP
|
||||
|
||||
Статус: принято, 20.10.2026.
|
||||
|
||||
## Решение
|
||||
|
||||
1. Админка Payload и её REST API работают только на `plc.veilix.cc` (prod) и `dev.plc.veilix.cc` (dev). На доменах брендов эти пути закрыты.
|
||||
2. nginx пускает на хосты админки только разрешённые IP.
|
||||
3. Свои эндпоинты приложения (ALTCHA, вебхуки, health) — под префиксом `/svc/`.
|
||||
|
||||
## Последствия
|
||||
|
||||
1. Адрес админки не связан с доменами брендов и закрыт от посторонних на уровне nginx.
|
||||
2. Сайты, кабинеты и MiniApp к хосту админки не обращаются, поэтому ограничение по IP касается только администраторов ([02-architecture.md](../02-architecture.md), раздел 2).
|
||||
3. Чтобы заходить в админку откуда угодно, в список разрешённых IP можно внести адреса своих VPN-серверов.
|
||||
@@ -0,0 +1,27 @@
|
||||
# Решения
|
||||
|
||||
По файлу на решение. Формат: контекст, решение, рассмотренные варианты, последствия. Новое решение — новый файл со следующим номером. Пересмотр — новый файл со ссылкой на старый, у старого статус «заменено».
|
||||
|
||||
| № | Решение | Статус |
|
||||
|---|---|---|
|
||||
| [D-001](D-001-single-app.md) | Одно Next.js-приложение на все домены | принято |
|
||||
| [D-002](D-002-payload-version.md) | Payload 3, переход на 4 после стабильного релиза | принято |
|
||||
| [D-003](D-003-hosting.md) | Без Docker: всё на хосте, процессы под systemd | принято |
|
||||
| [D-004](D-004-environments.md) | Dev и prod на одном сервере под разными пользователями | принято |
|
||||
| [D-005](D-005-multitenancy.md) | Сайты — тенанты, домены в базе, маршрутизация по Host | принято |
|
||||
| [D-006](D-006-localization.md) | Переводы: поля Payload и next-intl; 5 языков | принято |
|
||||
| [D-007](D-007-language-detection.md) | Определение языка; MaxMind через nginx | принято |
|
||||
| [D-008](D-008-uuid.md) | UUID и случайные публичные коды | принято |
|
||||
| [D-009](D-009-billing-model.md) | Заказы, платежи, движения по счёту; всё через баланс | принято |
|
||||
| [D-010](D-010-currencies.md) | Валюты и платёжные системы — сущности; платёжные системы привязаны к валютам | принято |
|
||||
| [D-011](D-011-accounts.md) | Раздельные аккаунты по сайтам | принято |
|
||||
| [D-012](D-012-auth.md) | Вход по коду через свою стратегию Payload; оформление заказа на my.* | принято |
|
||||
| [D-013](D-013-captcha.md) | Капча ALTCHA | принято |
|
||||
| [D-014](D-014-email.md) | Почта: шаблоны в Payload, переключаемый транспорт, без listmonk | принято |
|
||||
| [D-015](D-015-telegram-bots.md) | Боты на grammY без n8n; два бота на бренд | принято |
|
||||
| [D-016](D-016-payments.md) | Платёжные системы по валютам; автосписания — Stripe | принято |
|
||||
| [D-017](D-017-static-pages.md) | Статические маркетинговые страницы с пересборкой по событию | принято |
|
||||
| [D-018](D-018-analytics.md) | Umami на этом сервере и баннер cookies | принято |
|
||||
| [D-019](D-019-external-services.md) | Chatwoot и Twenty CRM на отдельном VPS | принято |
|
||||
| [D-020](D-020-key-models.md) | Модели ключей dedicated и mass | принято |
|
||||
| [D-021](D-021-admin-host.md) | Админка на plc.veilix.cc с доступом по IP | принято |
|
||||
@@ -0,0 +1,226 @@
|
||||
# Общее
|
||||
|
||||
Сервис продажи VPN. Ключи для приложений Happ (vless, histeria) и Wiregueard/Amnezia WG.
|
||||
|
||||
|
||||
|
||||
# Своими словами
|
||||
|
||||
Есть 2 сайта.
|
||||
|
||||
Один - продаёт Dedicated VPN, где каждому пользователю даётся свой IP.
|
||||
|
||||
Второй - более классический вариант, где на 1 IP множество пользователей.
|
||||
|
||||
Фундаментально они работают одинаково, это привязанные к пользователю ключи. Ключи уже открывают доступ либо к dedicated либо mass.
|
||||
|
||||
Скрипты - которыми обрабатывается оба сайта - общие.
|
||||
|
||||
Поэтому это одна база, один бэк, но разные домены. Отличаться они будут по доменам.
|
||||
|
||||
Дизайн отличается по цветам, но в общей концепции, но дизайны всё таки разные, тк по разному представлены данные и делать именно общий смысла нет.
|
||||
- Фиолетовый - это dedicated.
|
||||
- Зелёный - mass.
|
||||
- Дизайн сделал в Claude Design лежит в файле VEILIX-handoff.zip.
|
||||
|
||||
Сайт x2
|
||||
Личный кабинет x2
|
||||
Личный кабинет в Telegram (MiniApp) x2
|
||||
Бот в TG x2
|
||||
|
||||
veilix.app - dedicated
|
||||
veilixvpn.com - mass
|
||||
|
||||
my.veilix.app - личный кабинет dedicated
|
||||
my.veilixvpn.com - личный кабинет mass
|
||||
|
||||
tg.veilix.app - tg miniapp dedicated
|
||||
tg.veilixvpn.com - tg miniapp mass
|
||||
|
||||
Сайты многоязычные.
|
||||
|
||||
Боты чтобы и миниапп были или в самом tg можно было командами всё делать.
|
||||
|
||||
|
||||
|
||||
# Стэк
|
||||
|
||||
- CMS Payload
|
||||
- Next
|
||||
- React
|
||||
- SSR
|
||||
- Postgres
|
||||
|
||||
Вопрос. Payload через докер ставится? Нужно чтобы он с базой и nginx которые на хосте работал, чтобы не ставить базу в докер. Это нормально или лучше чтобы база в докере тоже была?
|
||||
|
||||
|
||||
|
||||
# Сущности
|
||||
|
||||
- Сайты
|
||||
- Домены
|
||||
- Языки
|
||||
- Страны
|
||||
- Пользователи
|
||||
- Тарифы
|
||||
- Ключи
|
||||
- Подключения
|
||||
- Заказы
|
||||
- Транзации счёта пользователя
|
||||
|
||||
|
||||
|
||||
# Сайты и Домены
|
||||
|
||||
Каждая из сущностей привязывается к Сайту.
|
||||
|
||||
Языки привязываются к Домену, Домены привязываются к сайтам.
|
||||
|
||||
Вопрос.
|
||||
В Payload связка сайты/языки может какой то нативный мехнизм есть?
|
||||
И в целом где-то переводы всего в базе тоже нужно иметь на разных языках или это есть тоже нативное?
|
||||
|
||||
|
||||
|
||||
# Языки
|
||||
|
||||
По умолчанию язык EN без постфикса в URL, остальные языки с postfix /de /es /jp /ru
|
||||
|
||||
Английский, немецкий, французский, испанский, португальский, японский и дальше RU.
|
||||
|
||||
Язык именно через URL везде проводить, те запомнить выбор можно в настройках пользователя или куки или localstorage, но в URL показывать именно с постфиксом.
|
||||
|
||||
Определять по MAXMIND LITE в Nginx.
|
||||
|
||||
|
||||
|
||||
# Telegram боты
|
||||
|
||||
Команды в ботах через n8n, чтобы можно было править логику ответов.
|
||||
|
||||
Вопрос.
|
||||
|
||||
n8n для этого подходит или есть другие варианты?
|
||||
Miniapp я думаю имеет смысл напрямую делать без n8n?
|
||||
|
||||
|
||||
|
||||
# Авторизация и регистрация
|
||||
|
||||
По email по коду без паролей.
|
||||
|
||||
Одно поле email, проверять есть или нет. Либо регистрировать либо авторизовывать.
|
||||
|
||||
Защить Google Captcha.
|
||||
|
||||
Отправка будет через локально установленный listmonk.
|
||||
|
||||
|
||||
|
||||
# Оплата, заказы, транзации
|
||||
|
||||
У пользователя есть счёт.
|
||||
|
||||
Его можно пополнить криптой и картой, может ещё как то в итоге.
|
||||
|
||||
Привязка карты к счёту и рекурентная оплата в зависимости от суммы всех ключей.
|
||||
|
||||
Если положить на счёт деньги заранее, то ежемесячно списывается только сумма за вычетом того что есть на счёте.
|
||||
|
||||
Понять как лучше сделать в плане связки Заказы и Транзакции.
|
||||
|
||||
Оплачивает клиент Заказ, к заказу справочно привязывается Тариф и Ключ и Пользователь.
|
||||
|
||||
Юкасса Страйп (демо режимы пока)
|
||||
|
||||
Вопрос.
|
||||
|
||||
Заказы и Транзакции - не понятно, может одной сущности достаточно.
|
||||
|
||||
Нужна возможность делать оплату вручную, те то что генерируется при оплате с карточки или при рекурентном списании, чтобы любые из этих действий я мог добавить в админке payload и иницировались остальные действия, пользователь получил ключ или начисления на счёт и тп. Может по какой то галочке - "Провести как реальную оплату", тогда это будет не просто запись в коллекции, а произойдёт весь набор действий, который при реальной оплате.
|
||||
|
||||
|
||||
|
||||
|
||||
# Ключи VPN
|
||||
|
||||
Страны привязыны к Тарифам.
|
||||
|
||||
Тэги привязаны к Тарифам.
|
||||
|
||||
Страны, чтобы на сайте отображать и возможно для получения с бэка в дальнейшем ключей.
|
||||
|
||||
Тэги, нужно будут для получения с бэка ключей для фильтраций каких-то произвольных.
|
||||
|
||||
Пользователь выбирает Тариф, на основе Тарифа ему подбирается свободный сервер и с бэка прилетают Подключения для ключа.
|
||||
|
||||
Подключения могут быть вручную добавлены, ссылка на подписку для happ, и ключ для wg.
|
||||
|
||||
Срок истечения у Ключа. Те следующий заказ продлевает этот же ключ.
|
||||
|
||||
К Ключу тоже привязывается Тариф, чтобы при достижении срока оплата списалась.
|
||||
|
||||
|
||||
|
||||
# ID и UUID
|
||||
|
||||
Не должно быть явных ID нигде, которые идут подряд, чтобы не было понятно, сколько и чего есть в базе.
|
||||
|
||||
Нужно использовать UUID, в тч при работе с API, чтобы и в консоли браузера нельзя было увидеть реальные ID.
|
||||
|
||||
Если payload предполагает автоикремент ID обычный, возможно не стоит городить кастом, и пусть так будет. Но нужно тогда всё равно всё обыграть все обращения по UUID.
|
||||
|
||||
Вопрос. Обсудить.
|
||||
|
||||
|
||||
|
||||
# Дополнительный софт
|
||||
|
||||
На этом же vps:
|
||||
|
||||
- N8N - для TG бота
|
||||
- Listmonk - отправка транзакционных писем
|
||||
|
||||
|
||||
На отдельном vps:
|
||||
|
||||
- Chatwoot - чат на сайте
|
||||
- 12CRM - лиды собирать
|
||||
|
||||
|
||||
|
||||
# Этапы
|
||||
|
||||
Составить всю .md документацию и план. Далее, чтобы я мог запускать по этапам в отдельных сессиях.
|
||||
|
||||
В первую очередь собрать полноценный сайт с дизайном, всем функционалом (регистрация, авторизация, реферальная программа). Без эквайринга и ключей по внешним api.
|
||||
|
||||
При этом чтобы вручную я мог и добавить ключи и оплаты.
|
||||
|
||||
Далее интеграция с chatwoot, 12crm.
|
||||
|
||||
Далее listmonk.
|
||||
|
||||
Далее TG боты.
|
||||
|
||||
Далее эквайринг и ключи. Но ключи автоматизация пока с вопросами, это нужно оставить обсуждение к моменту запуска этапа.
|
||||
|
||||
Эквайринг тестовый режим страйпа и юкассы, я зарегистриуюсь, если они дают песочницу, то сначала так.
|
||||
|
||||
|
||||
|
||||
# Общее
|
||||
|
||||
- Отраслевые стандарты. Стараться придерживаться отраслевых стандартов и использовать максимально современные решения. Если я предлагаю решения, которые плохо связываются друг с другом или кастом, который может решается иными методами, то обсудить со мной. Я могу не знать, но с другой стороны может быть что нужно и делать кастом, в любом случае это повод для обсуждения.
|
||||
|
||||
- Нумерация пунктов в документации и чатаха с тобой - цифрами и латинскими буквами, чтобы не было путаницы, где А русская, где анлийская, гда B - толи БЭ латинская, толи ВЭ русская.
|
||||
|
||||
- Не писать никаких скриптов пока не будет всё утверждено, сначала обсуждаем.
|
||||
|
||||
- Если есть противоречия на любом этапе, сначала обсуждаем, потом делаем.
|
||||
|
||||
- Софт, который не достаёт и требуется установки от root - говоришь, я сделаю. Не нужно пытаться какие то обходные пути делать внутри пользователя, чтобы меня не беспокоить. Что нужно и правильно установить глобально на сервере - я установлю.
|
||||
|
||||
- Настройки на сервере - также как и софт, что правильнее сделать глобально, делаю глобально я.
|
||||
|
||||
- Доступ к root у меня есть, я в этом разбираюсь, нет никаких проблем и софт и настройки сделать.
|
||||
@@ -0,0 +1,19 @@
|
||||
# Этап 0. Документация
|
||||
|
||||
Статус: готово 20.10.2026.
|
||||
|
||||
## 1. Цель
|
||||
|
||||
Зафиксировать продукт, архитектуру, модель данных и план этапов так, чтобы каждый этап можно было запускать в отдельной сессии.
|
||||
|
||||
## 2. Сделано
|
||||
|
||||
1. Обсуждение исходной задачи ([history/HUMAN_IDEA.md](../history/HUMAN_IDEA.md)) и макетов, решения [D-001…D-021](../decisions/README.md).
|
||||
2. Документы `docs/01…10`, файлы этапов, [CLAUDE.md](../../CLAUDE.md).
|
||||
3. Макеты скопированы в `design/handoff/`.
|
||||
4. Репозиторий инициализирован локально, без коммитов.
|
||||
|
||||
## 3. Осталось
|
||||
|
||||
1. Переезд разработки под пользователя vx_dev (R-00), репозиторий в Gitea и SSH-доступ для vx_dev (R-01), затем первый коммит и push по просьбе пользователя.
|
||||
2. Вопросы к следующим этапам — в [00-index.md](../00-index.md), раздел 4.2. Этап 1a они не блокируют.
|
||||
@@ -0,0 +1,39 @@
|
||||
# Этап 1a. Фундамент
|
||||
|
||||
Статус: не начат.
|
||||
|
||||
## 1. Цель
|
||||
|
||||
Рабочий каркас: одно приложение отвечает на все dev-домены, определяет сайт, язык и валюту, показывает оболочку обоих брендов в своих цветах; админка с базовыми справочниками.
|
||||
|
||||
## 2. Входит
|
||||
|
||||
1. Проект: Next.js 16.4, Payload 3.x (или 4, если вышел стабильный), TypeScript strict, pnpm, ESLint, Prettier. Структура папок — [02-architecture.md](../02-architecture.md), раздел 3.
|
||||
2. PostgreSQL: адаптер с `idType: 'uuid'`, push в dev, настроенные миграции для prod.
|
||||
3. Payload: админка только на хосте админки; `admins` с ролями; плагин multi-tenant (поле `site`); локализация `en, ru, de, fr, es` с откатом на `en`; интерфейс админки `ru` и `en`; `filterAvailableLocales`.
|
||||
4. Коллекции: `sites`, `domains`, `currencies`, `countries`, `locations`, `tags`, `tariffs`, `payment-systems` (пока только адаптер `manual`), `media` — по [03-data-model.md](../03-data-model.md).
|
||||
5. Proxy: Host → домен → сайт и тип фронтенда; префиксы языков (next-intl, английский без префикса); определение языка по [06-i18n.md](../06-i18n.md), раздел 4; cookie валюты; внутренние маршруты; закрыть `/admin` и REST Payload на доменах брендов.
|
||||
6. Темы: токены брендов ([07-frontend.md](../07-frontend.md), раздел 4), Tailwind v4, `data-site`, `data-theme`, три режима темы без мигания; шрифты через `next/font`.
|
||||
7. Оболочки: шапка и подвал маркетинговых сайтов обоих брендов с переключателями языка, валюты и темы; оболочка кабинета (боковая панель и мобильные вкладки) с заглушками страниц.
|
||||
8. Начальные данные (seed): 2 сайта, dev-домены, валюты, 30 стран и RU, 30 локаций на каждом сайте, тарифы dedicated (3 уровня) и mass (месяц, год) с ценами в RUB и USD, платёжная система `manual`.
|
||||
9. `/svc/health`.
|
||||
10. systemd user-юнит `veilix-dev.service`; текст конфигурации nginx для dev-хостов на согласование.
|
||||
11. Раздел «Команды» в [CLAUDE.md](../../CLAUDE.md).
|
||||
|
||||
## 3. Не входит
|
||||
|
||||
Страницы по макетам (1b), вход (1c), биллинг (1d).
|
||||
|
||||
## 4. Перед стартом
|
||||
|
||||
Root: R-00…R-06 ([09-infrastructure.md](../09-infrastructure.md), раздел 8). Открытых решений к этапу нет.
|
||||
|
||||
## 5. Готово, когда
|
||||
|
||||
1. `dev.veilix.app` и `dev.veilixvpn.com` открываются по HTTPS: шапка и подвал в цветах бренда, три режима темы.
|
||||
2. Работают `/ru`, `/de`, `/fr`, `/es`; первый заход без префикса перенаправляет по правилам определения языка; поисковые боты не перенаправляются.
|
||||
3. Переключатель валюты запоминает выбор.
|
||||
4. `dev.plc.veilix.cc/admin`: переключение сайтов, справочники редактируются, локализованные поля переключаются по языкам, у сайта видны только его языки.
|
||||
5. В URL админки — UUID.
|
||||
6. `dev.my.*` показывает оболочку кабинета; `/admin` на доменах брендов — 404.
|
||||
7. `pnpm build`, линтер и проверка типов проходят.
|
||||
@@ -0,0 +1,34 @@
|
||||
# Этап 1b. Маркетинговые страницы
|
||||
|
||||
Статус: не начат.
|
||||
|
||||
## 1. Цель
|
||||
|
||||
Сайты обоих брендов по макетам, с контентом из Payload, формами и SEO.
|
||||
|
||||
## 2. Входит
|
||||
|
||||
1. Страницы по макетам для обоих брендов: главная, цены, локации (карта, отрисованная на сервере), поддержка ([07-frontend.md](../07-frontend.md), раздел 2). Юридические страницы `/legal/*`, 404 и 500.
|
||||
2. Коллекции `pages`, `faq` (с местами показа), `leads`; поля контактов в `sites`.
|
||||
3. Контент из макетов на RU и EN: тексты, FAQ, локации, тарифы.
|
||||
4. Черновики юридических текстов на RU и EN с заглушками вместо данных юрлица: условия, конфиденциальность, возвраты, cookies.
|
||||
5. ALTCHA ([08-integrations.md](../08-integrations.md), раздел 2). Формы «Индивидуальные условия» и обратной связи с ALTCHA пишут в `leads`. Письмо администратору появится в 1c.
|
||||
6. SEO: `<title>` и описания на каждом языке, `hreflang`, `canonical`, `sitemap.xml` и `robots.txt` на каждый домен, картинки для соцсетей.
|
||||
7. Статическая сборка «язык × валюта» и пересборка по событию из Payload ([D-017](../decisions/D-017-static-pages.md)).
|
||||
8. Решить, показывать ли количество свободных серверов.
|
||||
|
||||
## 3. Не входит
|
||||
|
||||
Оформление заказа (1d), кабинет (1f), переводы на de, fr, es (1h; до этого — английский запасной).
|
||||
|
||||
## 4. Перед стартом
|
||||
|
||||
Q-02 (цены mass в USD), Q-08 (email поддержки), Q-09 (часовой пояс), Q-16 (ящик поддержки).
|
||||
|
||||
## 5. Готово, когда
|
||||
|
||||
1. Страницы обоих брендов совпадают с макетами: светлая и тёмная тема, десктоп и мобильный, RU и EN.
|
||||
2. Правка цены, статуса локации или FAQ в админке видна на сайте в течение нескольких секунд.
|
||||
3. Lighthouse mobile ≥ 90 на главной и странице цен обоих брендов.
|
||||
4. Формы создают заявки, ALTCHA работает.
|
||||
5. `sitemap.xml` и `hreflang` проходят проверку.
|
||||
@@ -0,0 +1,32 @@
|
||||
# Этап 1c. Вход по коду и почта
|
||||
|
||||
Статус: не начат.
|
||||
|
||||
## 1. Цель
|
||||
|
||||
Регистрация и вход по коду из письма в кабинетах обоих брендов; почтовая подсистема с журналом.
|
||||
|
||||
## 2. Входит
|
||||
|
||||
1. Почта ([08-integrations.md](../08-integrations.md), раздел 1): глобал «Почта» (транспорты `smtp`, `sendmail`, `log_only`), email-адаптер, `email-templates` с шапкой и подвалом бренда, `email-log`, задача `send-email`, тестовое письмо, флаг «Записывать коды входа в журнал».
|
||||
2. Шаблоны на RU и EN: `login_code`, `admin_new_lead` (и подключить его к формам из 1b).
|
||||
3. `customers` со своей стратегией, `customer-sessions`, `auth-codes`; запрос и проверка кода, лимиты, ALTCHA ([05-auth.md](../05-auth.md), разделы 2–4).
|
||||
4. Страница `/login` по макету с состояниями: код отправлен, ошибка, истёк, повторная отправка через 60 секунд.
|
||||
5. Выход, выход на всех устройствах.
|
||||
6. Язык и валюта аккаунта при регистрации ([04-billing.md](../04-billing.md), раздел 2).
|
||||
7. Задача `cleanup` для кодов, сессий и журнала.
|
||||
|
||||
## 3. Не входит
|
||||
|
||||
Telegram (этап 3), реферальная привязка при регистрации (1g).
|
||||
|
||||
## 4. Перед стартом
|
||||
|
||||
Root: R-07 (Postfix, OpenDKIM, DNS) или данные SMTP-провайдера. Без них этап делается на транспорте `log_only`.
|
||||
|
||||
## 5. Готово, когда
|
||||
|
||||
1. Регистрация и вход работают на `dev.my.veilix.app` и `dev.my.veilixvpn.com`; аккаунты сайтов независимы.
|
||||
2. С флагом коды видны в журнале; на `log_only` письма не уходят.
|
||||
3. Через `sendmail` или `smtp` письмо доходит до Gmail не в спам; оценка mail-tester ≥ 9/10.
|
||||
4. Лимиты и счётчик попыток работают; сессии отзываются.
|
||||
@@ -0,0 +1,36 @@
|
||||
# Этап 1d. Биллинг без эквайринга
|
||||
|
||||
Статус: не начат.
|
||||
|
||||
## 1. Цель
|
||||
|
||||
Деньги и заказы работают целиком: оформление заказа, оплата с баланса, ручные платежи, продления, льготный период — без внешних платёжных систем.
|
||||
|
||||
## 2. Входит
|
||||
|
||||
1. Коллекции `orders`, `payments`, `ledger-entries`, `payment-methods` (структура), `keys` (поля и статусы), `audit-log`.
|
||||
2. `core/billing`: `processPayment`, оплата с баланса, задачи `renewals`, `expiry`, `reminders`, `reconcile`; блокировки и уникальные ограничения ([04-billing.md](../04-billing.md)).
|
||||
3. `/order` по макетам обоих брендов: шаг «Почта» из 1c, шаг «Оплата» (с баланса; «Заказ создан, ожидает оплаты», когда платёжных систем нет), согласие с условиями, сводка из тарифа, локации и валюты.
|
||||
4. `/buy` в кабинете: dedicated — сетка локаций с ценами, mass — «Месяц» и «Год».
|
||||
5. Админка: ручной платёж, корректировка баланса, возврат на баланс, продление вручную, блокировка, «Очередь выдачи»; всё — в журнал действий.
|
||||
6. Шаблоны писем на RU и EN: `order_paid`, `key_provisioning`, `renewal_reminder`, `expiry_reminder`, `renewal_succeeded`, `renewal_failed`, `grace_started`, `key_expired`, `topup_succeeded`, `refund_issued`, `admin_new_order`, `admin_provisioning_needed`.
|
||||
|
||||
## 3. Не входит
|
||||
|
||||
Подключения и выдача ключа (1e), реальные платёжные системы (этап 4). Для проверок ключ переводится в `active` вручную в админке.
|
||||
|
||||
## 4. Перед стартом
|
||||
|
||||
Этап 1c готов.
|
||||
|
||||
## 5. Готово, когда
|
||||
|
||||
Автотесты `core/billing` проходят сценарии:
|
||||
|
||||
1. Ручное пополнение → оплата заказа с баланса → заказ оплачен → ключ `provisioning`.
|
||||
2. Частичный баланс + ручной платёж на разницу → заказ оплачен.
|
||||
3. Продление с баланса за 24 часа; денег нет → заказ ждёт → пополнение само оплачивает продление.
|
||||
4. Переходы в льготный период и «истёк» с письмами.
|
||||
5. Частичный и полный возврат на баланс, с отменой ключа и без.
|
||||
6. Повторный `processPayment` и параллельные запуски задач не списывают дважды.
|
||||
7. `reconcile` не находит расхождений.
|
||||
@@ -0,0 +1,26 @@
|
||||
# Этап 1e. Ключи и подключения
|
||||
|
||||
Статус: не начат.
|
||||
|
||||
## 1. Цель
|
||||
|
||||
Администратор выдаёт ключи вручную; пользователь получает конфиги и ссылки на странице ключа.
|
||||
|
||||
## 2. Входит
|
||||
|
||||
1. Коллекция `connections` с шифрованием содержимого ([03-data-model.md](../03-data-model.md), 5.2).
|
||||
2. Админка: на странице ключа — добавление подключений (WG/AWG — файлом или текстом, ссылка Happ, ссылка Xray, RU-адрес, номер устройства, страна), действие «Выдать ключ», список «К отключению».
|
||||
3. Страница ключа в кабинете: срок, галочка автопродления, «Продлить сейчас», WG-конфиги (скачать, QR), Happ/Xray (копировать, QR); для mass — WG по странам: конфиги, которые добавил администратор; нужна другая страна — ссылка на поддержку.
|
||||
4. Состояния: готовится, льготный период, истёк, нет подключений.
|
||||
5. Содержимое конфигов отдаётся отдельным запросом только владельцу, в HTML страницы не попадает. QR строится в браузере из полученного содержимого.
|
||||
6. Шаблон `key_ready` на RU и EN.
|
||||
|
||||
## 3. Перед стартом
|
||||
|
||||
Этап 1d готов.
|
||||
|
||||
## 4. Готово, когда
|
||||
|
||||
1. Администратор выдал ключ — пользователь видит его активным, срок пошёл с момента выдачи.
|
||||
2. Скачанный `.conf` импортируется в WireGuard; QR сканируется в WireGuard и Happ.
|
||||
3. Чужой пользователь получает 404 на конфиг (тест).
|
||||
@@ -0,0 +1,24 @@
|
||||
# Этап 1f. Веб-кабинеты
|
||||
|
||||
Статус: не начат.
|
||||
|
||||
## 1. Цель
|
||||
|
||||
Кабинеты обоих брендов по макетам `App` и `MassApp`.
|
||||
|
||||
## 2. Входит
|
||||
|
||||
1. Главная: ключи, «Купить ключ», FAQ.
|
||||
2. Список ключей; бейдж «Истекает» по правилу [04-billing.md](../04-billing.md), раздел 15.
|
||||
3. «Счёт и платежи»: баланс, пополнение (пресеты и своя сумма; пока платёжных систем нет — понятное сообщение), сохранённая карта и «Отвязать карту» (видно с этапа 4), ближайшие списания, история.
|
||||
4. Настройки: язык, валюта (правило — [04-billing.md](../04-billing.md), раздел 2), тема; выход.
|
||||
5. Мобильная навигация: вкладки «Главная, Ключи, Купить, Ещё».
|
||||
6. Различия брендов: у mass «Список ключей» без «текущей/запасной локации».
|
||||
|
||||
## 3. Не входит
|
||||
|
||||
Реферальная страница (1g), MiniApp (этап 3).
|
||||
|
||||
## 4. Готово, когда
|
||||
|
||||
Кабинеты обоих брендов совпадают с макетами: светлая и тёмная тема, десктоп и мобильный, RU и EN. Все данные — реальные, из базы.
|
||||
@@ -0,0 +1,23 @@
|
||||
# Этап 1g. Реферальная программа
|
||||
|
||||
Статус: не начат.
|
||||
|
||||
## 1. Цель
|
||||
|
||||
«Приведите друга — вы оба получите по месяцу в подарок» по правилам [04-billing.md](../04-billing.md), раздел 11.
|
||||
|
||||
## 2. Входит
|
||||
|
||||
1. Реферальные коды пользователей.
|
||||
2. Маршрут `my.<домен>/r/<код>`: cookie `vx_ref` на 60 дней и переход на сайт бренда.
|
||||
3. Привязка при регистрации; коллекция `referrals`.
|
||||
4. Бонус обоим после первой оплаты заказа `new_key` приглашённого; статус `review` при совпадении хэша IP.
|
||||
5. Суммы бонуса по валютам в `sites`.
|
||||
6. Страница `/referral` по макету: приглашено, начислено, ожидает; ссылка с копированием; приглашённые с замаскированной почтой.
|
||||
7. Шаблон `referral_reward` на RU и EN; список приглашений в админке.
|
||||
|
||||
## 3. Готово, когда
|
||||
|
||||
1. A делится ссылкой → B регистрируется → B оплачивает первый заказ → обоим начислен бонус.
|
||||
2. Самоприглашение с того же IP попадает в `review`.
|
||||
3. Ссылка другого бренда не засчитывается.
|
||||
@@ -0,0 +1,26 @@
|
||||
# Этап 1h. Переводы, аналитика, запуск prod
|
||||
|
||||
Статус: не начат.
|
||||
|
||||
## 1. Цель
|
||||
|
||||
Сайты и кабинеты на 5 языках работают на основных доменах.
|
||||
|
||||
## 2. Входит
|
||||
|
||||
1. Переводы de, fr, es: интерфейс и контент Payload, машинный перевод LLM с глоссарием ([06-i18n.md](../06-i18n.md), раздел 7); проверка непереведённых ключей.
|
||||
2. Umami на этом сервере ([D-018](../decisions/D-018-analytics.md)) с проксированием через домены брендов; баннер cookies ([08-integrations.md](../08-integrations.md), раздел 8).
|
||||
3. Проверка производительности (Lighthouse mobile ≥ 90) и доступности.
|
||||
4. Prod под пользователем vx_prod: `.env`, команда выкладки с папками релизов и откатом ([02-architecture.md](../02-architecture.md), раздел 5), первая выкладка, начальные данные prod, первый администратор, smoke-тесты.
|
||||
5. Бэкапы базы и media с проверкой восстановления.
|
||||
6. Юридические черновики на сайтах (Q-13 — пока с заглушками).
|
||||
|
||||
## 3. Перед стартом
|
||||
|
||||
Root: R-08…R-11, по желанию R-15. Решения: Q-14 (куда бэкапы), Q-18 (кто запускает выкладку).
|
||||
|
||||
## 4. Готово, когда
|
||||
|
||||
1. Основные домены работают по HTTPS, dev и prod разделены.
|
||||
2. Выкладка и откат отрепетированы.
|
||||
3. Восстановление из бэкапа проверено.
|
||||
@@ -0,0 +1,19 @@
|
||||
# Этап 2. Chatwoot и Twenty CRM
|
||||
|
||||
Статус: не начат.
|
||||
|
||||
## 1. Входит
|
||||
|
||||
1. Chatwoot ([08-integrations.md](../08-integrations.md), раздел 4): виджет на сайтах и в кабинетах, отдельные входящие каналы для каждого бренда; идентификация вошедших пользователей с HMAC; категория в баннере cookies.
|
||||
2. Боты поддержки обоих брендов подключены к Chatwoot как Telegram-каналы.
|
||||
3. Почта поддержки (Q-16) — по решению, например email-канал Chatwoot.
|
||||
4. Twenty ([08-integrations.md](../08-integrations.md), раздел 5): заявки уходят через API фоновой задачей с повторами, `leads.crmId`.
|
||||
|
||||
## 2. Перед стартом
|
||||
|
||||
Root: R-12 — VPS, Chatwoot, Twenty, ключи API, боты поддержки в BotFather.
|
||||
|
||||
## 3. Готово, когда
|
||||
|
||||
1. Сообщения из виджетов и ботов поддержки приходят в Chatwoot с указанием бренда; вошедший пользователь опознан.
|
||||
2. Заявки появляются в Twenty.
|
||||
@@ -0,0 +1,23 @@
|
||||
# Этап 3. Telegram
|
||||
|
||||
Статус: не начат.
|
||||
|
||||
## 1. Входит
|
||||
|
||||
1. Основные боты обоих брендов на grammY: вебхук, команды, меню, тексты в Payload на всех языках ([08-integrations.md](../08-integrations.md), раздел 6).
|
||||
2. MiniApp по макетам `AppTG` и `MassAppTG` с Telegram SDK: BackButton, MainButton, тема Telegram, отклик вибрацией; вход по `initData`.
|
||||
3. Привязка почты из Telegram, «Подключить Telegram» в кабинете, «Войти через Telegram» на сайте, объединение аккаунтов ([05-auth.md](../05-auth.md), раздел 6).
|
||||
4. Уведомления в бот; реферальный код через `/start`.
|
||||
5. Тестовые боты для dev с доступом по списку Telegram-аккаунтов.
|
||||
6. Оплата в Telegram — по решению Q-11.
|
||||
|
||||
## 2. Перед стартом
|
||||
|
||||
1. Root: R-13 — основные и тестовые боты, домен для Telegram Login, адреса MiniApp.
|
||||
2. Решения: Q-10 (имена ботов), Q-11 (оплата в Telegram).
|
||||
|
||||
## 3. Готово, когда
|
||||
|
||||
1. Из Telegram можно зарегистрироваться, купить ключ (по Q-11), получить конфиги, пополнить баланс, пригласить друга.
|
||||
2. Уведомления приходят в бот.
|
||||
3. Сценарии привязки и объединения аккаунтов проходят.
|
||||
@@ -0,0 +1,26 @@
|
||||
# Этап 4. Платёжные системы
|
||||
|
||||
Статус: не начат.
|
||||
|
||||
## 1. Входит
|
||||
|
||||
1. Адаптеры ([08-integrations.md](../08-integrations.md), раздел 7):
|
||||
a. Stripe: Payment Element, сохранение карты, автосписания на переменную сумму, возвраты, отвязка карты, вебхуки;
|
||||
b. RUB: процессинги для карт РФ — пополнения (провайдеров выбираем на старте, Q-12);
|
||||
c. криптовалюта — пополнения, экран счёта с адресом, суммой и таймером курса;
|
||||
d. ЮKassa — опционально, пополнения.
|
||||
2. Состояния оформления и пополнения: 3-D Secure, отказ, ожидание подтверждения сети.
|
||||
3. Повторы автосписаний ([04-billing.md](../04-billing.md), 6.1); возврат на карту (`refund` + `payout`).
|
||||
4. Тексты согласия на автосписания в оформлении заказа и в юридических документах.
|
||||
5. Настройка платёжных систем в админке: валюты, назначения, лимиты, тестовый или боевой режим. Если бренды получат разные аккаунты одного провайдера — Q-17.
|
||||
|
||||
## 2. Перед стартом
|
||||
|
||||
1. Root: R-14 — аккаунты провайдеров (сначала тестовые).
|
||||
2. Решения: Q-12; для боевого Stripe нужны данные юрлица (Q-13).
|
||||
|
||||
## 3. Готово, когда
|
||||
|
||||
1. Каждый адаптер в тестовом режиме проходит путь от оплаты до зачисления, включая повторную доставку вебхуков.
|
||||
2. Автопродление через Stripe работает на тестовых картах. Если карта требует 3-D Secure, пользователь получает письмо оплатить вручную.
|
||||
3. Возвраты на карту работают.
|
||||
@@ -0,0 +1,13 @@
|
||||
# Этап 5. Автоматизация выдачи ключей
|
||||
|
||||
Статус: обсуждение при старте этапа.
|
||||
|
||||
## 1. Что известно
|
||||
|
||||
1. По тарифу (страны, теги) подбирается свободный сервер, бэкенд возвращает подключения для ключа.
|
||||
2. Автоматика создаёт те же подключения, что на этапе 1 добавляет администратор ([D-020](../decisions/D-020-key-models.md)), и отключает их, когда ключ истекает.
|
||||
3. Наличие в локациях обновляется автоматически, сайты пересобираются по событию.
|
||||
|
||||
## 2. Обсудить при старте
|
||||
|
||||
Источник серверов и подключений, способ интеграции, как получать WG-конфиги mass по странам, обработка ошибок выдачи.
|
||||