refactor: project architecture refactor, container splitting

This commit is contained in:
3252a8
2026-05-17 00:01:28 +03:00
parent e0b5218037
commit 30fb774d93
367 changed files with 2609 additions and 864 deletions
@@ -0,0 +1,117 @@
<script>
import { CircleX, Plus, RefreshCw, Smartphone } from "$components/ui/icons.js";
import Button from "$components/ui/button.svelte";
import Card from "$components/ui/card.svelte";
import { EmptyCard, LinearProgress, StatusMessage } from "$components/patterns/webapp/index.js";
import {
devicesCountLabel,
devicesLimitLabel,
devicesPercent,
} from "../../lib/webapp/devicesLabels.js";
export let devicesBusy = false;
export let devicesData = {};
export let devicesIsError = false;
export let devicesLoaded = false;
export let devicesStatus = "";
export let subscription = {};
export let loadDevices = () => {};
export let openDeviceDisconnectDialog = () => {};
export let openDeviceTopupModal = () => {};
export let t = (key) => key;
</script>
<main class="content with-nav">
<Card class="devices-summary-card">
<div class="devices-summary-head">
<Smartphone size={28} />
<span>
<strong>{t("wa_devices_title")}</strong>
<small>{devicesCountLabel(devicesData, t)}</small>
</span>
<Button
variant="icon"
size="icon"
onclick={() => loadDevices(true)}
disabled={devicesBusy}
aria-label={t("wa_devices_refresh")}
>
<RefreshCw size={18} />
</Button>
</div>
<LinearProgress
class="devices-progress"
value={devicesPercent(devicesData)}
label={t("wa_devices_title")}
/>
{#if subscription?.active && subscription?.max_devices !== 0}
<Button variant="secondary" class="wide" onclick={openDeviceTopupModal}>
<Plus size={17} />
{t("wa_buy_hwid_devices")}
</Button>
{/if}
</Card>
{#if devicesBusy && !devicesLoaded}
<EmptyCard>{t("wa_devices_loading")}</EmptyCard>
{:else if devicesStatus}
<EmptyCard>
<StatusMessage error={devicesIsError}>{devicesStatus}</StatusMessage>
</EmptyCard>
{:else if !devicesData?.devices?.length}
<EmptyCard class="devices-empty-card">
<Smartphone size={28} />
<span>{t("wa_devices_empty")}</span>
<small>{t("wa_devices_empty_hint", { max: devicesLimitLabel(devicesData, t) })}</small>
</EmptyCard>
{:else}
<div class="devices-list">
{#each devicesData.devices as device (device.token || device.index)}
<Card class="device-card">
<div class="device-card-head">
<div class="device-icon"><Smartphone size={20} /></div>
<span>
<strong
>{device.display_name ||
t("wa_device_fallback_name", { index: device.index })}</strong
>
<small>{device.platform_label || t("wa_devices_platform_unknown")}</small>
</span>
</div>
<div class="device-meta">
{#if device.created_at_text}
<div>
<span>{t("wa_devices_connected_at")}</span>
<strong>{device.created_at_text}</strong>
</div>
{/if}
{#if device.hwid_short}
<div>
<span>HWID</span>
<code>{device.hwid_short}</code>
</div>
{/if}
{#if device.user_agent}
<div class="device-user-agent">
<span>User Agent</span>
<small>{device.user_agent}</small>
</div>
{/if}
</div>
{#if device.can_disconnect}
<Button
variant="outline"
class="wide device-disconnect-button"
onclick={() => openDeviceDisconnectDialog(device)}
>
<CircleX size={17} />
{t("wa_devices_disconnect")}
</Button>
{/if}
</Card>
{/each}
</div>
{/if}
</main>
@@ -0,0 +1,285 @@
<script>
import {
CheckCircle2,
ChevronsUpDown,
CircleX,
Database,
Download,
Gift,
RefreshCw,
} from "$components/ui/icons.js";
import BrandMark from "$lib/webapp/BrandMark.svelte";
import Button from "$components/ui/button.svelte";
import Card from "$components/ui/card.svelte";
import { LinearProgress } from "$components/patterns/webapp/index.js";
import { formatTrafficGb } from "../../lib/webapp/formatters.js";
import {
trafficPercent as trafficPercentFn,
trafficLabel as trafficLabelFn,
trafficResetLabel as trafficResetLabelFn,
premiumTrafficPercent as premiumTrafficPercentFn,
premiumTrafficLabel as premiumTrafficLabelFn,
premiumTitle as premiumTitleFn,
premiumServerLabels as premiumServerLabelsFn,
activeSubscriptionTermLabel as activeSubscriptionTermLabelFn,
} from "../../lib/webapp/traffic.js";
export let appSettings = {};
export let brand = {};
export let brandTitle = "";
export let canChangeTariff = false;
export let premiumTrafficTopupBarClickable = false;
export let premiumTrafficTopupUnlocked = false;
export let regularTrafficTopupBarClickable = false;
export let regularTrafficTopupUnlocked = false;
export let currentTariffName = "";
export let hasActiveTariffSubscription = false;
export let hasMultipleTariffs = false;
export let subscription = {};
export let trafficMode = false;
export let trialBusy = false;
export let termUnitLabel = () => ""; // We need this passed from App or context. Actually, App.svelte doesn't pass it yet. We'll pass it.
function trafficPercent(sub) {
return trafficPercentFn(sub);
}
function trafficLabel(sub) {
return trafficLabelFn(sub, t);
}
function trafficResetLabel(sub) {
return trafficResetLabelFn(sub, t);
}
function premiumTrafficPercent(sub) {
return premiumTrafficPercentFn(sub);
}
function premiumTrafficLabel(sub) {
return premiumTrafficLabelFn(sub, t);
}
function premiumTitle(sub = subscription) {
return premiumTitleFn(sub, t);
}
function premiumServerLabels(sub) {
return premiumServerLabelsFn(sub);
}
function activeSubscriptionTermLabel(sub) {
return activeSubscriptionTermLabelFn(sub, { t, termUnitLabel });
}
function trialTrafficLabel() {
const limit = Number(appSettings?.trial_traffic_limit_gb || 0);
return limit > 0 ? formatTrafficGb(limit) : t("wa_unlimited_traffic");
}
export let activateTrial = () => {};
export let openConnectLink = () => {};
export let openPaymentModal = () => {};
export let openRegularTopupModal = () => {};
export let openPremiumTopupModal = () => {};
export let openTariffChangeModal = () => {};
export let primaryPayActionLabel = () => "";
export let t = (key) => key;
</script>
<main class="home-layout">
<div class="login-brand home-brand">
<BrandMark {brand} size="xl" />
<h1>{brandTitle}</h1>
</div>
<div class="home-bottom">
<Card class={`status-card${subscription.active ? "" : " status-card-inactive"}`}>
{#if subscription.active}
<div class="sub-status">
<CheckCircle2 size={23} />
<div>
<h2>
{trafficMode ? t("wa_home_access_active") : t("wa_home_subscription_active")} | {activeSubscriptionTermLabel(
subscription
)}
</h2>
{#if hasActiveTariffSubscription && hasMultipleTariffs && currentTariffName}
<p class="current-tariff-line">
{t("wa_current_tariff", { tariff: currentTariffName })}
</p>
{/if}
<p>
{subscription.end_date_text
? t("wa_until_date", { date: subscription.end_date_text })
: subscription.remaining_text}
</p>
</div>
</div>
{:else}
<div class="sub-status sub-status-inactive">
<CircleX size={23} />
<h2>{t("wa_home_subscription_inactive")}</h2>
</div>
{/if}
</Card>
{#if subscription.active}
<Card class={regularTrafficTopupBarClickable ? "traffic-card-clickable" : ""}>
{#if regularTrafficTopupBarClickable}
<button
class="card-click-target"
type="button"
onclick={openRegularTopupModal}
aria-label={t("wa_add_traffic")}
></button>
{/if}
<div class="traffic-top">
<span>{t("wa_home_traffic_used")}</span>
<strong>{trafficLabel(subscription)}</strong>
</div>
<LinearProgress value={trafficPercent(subscription)} label={t("wa_home_traffic_used")} />
<div class="traffic-meta">
<span>{trafficResetLabel(subscription)}</span>
<span class="traffic-percent">{trafficPercent(subscription)}%</span>
</div>
</Card>
{#if subscription?.premium_unlimited_override}
<Card class="premium-traffic-card">
<div class="traffic-top">
<span>{premiumTitle(subscription)}</span>
<strong></strong>
</div>
<div class="traffic-meta premium-traffic-meta">
{#if premiumServerLabels(subscription).length}
<details class="premium-server-dropdown">
<summary>
<span>{t("wa_premium_unlimited", {}, "Безлимит на premium-сервера")}</span>
<ChevronsUpDown size={13} />
</summary>
<div class="premium-server-list premium-server-list-dropdown">
<small
>{t("wa_premium_servers_limited", {}, "Отдельный лимит действует на")}</small
>
<div>
{#each premiumServerLabels(subscription).slice(0, 8) as label}
<span>{label}</span>
{/each}
</div>
</div>
</details>
{:else}
<span>{t("wa_premium_unlimited", {}, "Безлимит на premium-сервера")}</span>
{/if}
</div>
</Card>
{:else if Number(subscription?.premium_limit_bytes || 0) > 0}
<Card
class={`${premiumTrafficTopupBarClickable ? "traffic-card-clickable " : ""}premium-traffic-card${subscription?.premium_is_limited ? " premium-traffic-card-limited" : ""}`}
>
{#if premiumTrafficTopupBarClickable}
<button
class="card-click-target"
type="button"
onclick={openPremiumTopupModal}
aria-label={t("wa_add_traffic_premium", { target: premiumTitle(subscription) })}
></button>
{/if}
<div class="traffic-top">
<span>{premiumTitle(subscription)}</span>
<strong>{premiumTrafficLabel(subscription)}</strong>
</div>
<LinearProgress
class="premium-progress"
value={premiumTrafficPercent(subscription)}
label={premiumTitle(subscription)}
/>
<div class="traffic-meta premium-traffic-meta">
{#if premiumServerLabels(subscription).length}
<details class="premium-server-dropdown">
<summary>
<span
>{subscription?.premium_is_limited
? t("wa_premium_access_limited", {}, "Доступ к premium временно ограничен")
: t("wa_premium_reset_monthly", {}, "Отдельный лимит на месяц")}</span
>
<ChevronsUpDown size={13} />
</summary>
<div class="premium-server-list premium-server-list-dropdown">
<small
>{t("wa_premium_servers_limited", {}, "Отдельный лимит действует на")}</small
>
<div>
{#each premiumServerLabels(subscription).slice(0, 8) as label}
<span>{label}</span>
{/each}
</div>
</div>
</details>
{:else}
<span
>{subscription?.premium_is_limited
? t("wa_premium_access_limited", {}, "Доступ к premium временно ограничен")
: t("wa_premium_reset_monthly", {}, "Отдельный лимит на месяц")}</span
>
{/if}
<span class="traffic-percent">{premiumTrafficPercent(subscription)}%</span>
</div>
</Card>
{/if}
{:else if appSettings?.trial_enabled && appSettings?.trial_available}
<Card class="trial-card">
<div class="trial-card-head">
<Gift size={22} />
<span>
<strong>{t("wa_trial_title")}</strong>
<small
>{t("wa_trial_details", {
days: Number(appSettings?.trial_duration_days || 0),
traffic: trialTrafficLabel(),
})}</small
>
</span>
</div>
</Card>
{/if}
<div class="action-stack">
{#if subscription.active}
<Button class="wide" onclick={openConnectLink}>
<Download size={18} />
{t("wa_install_and_configure")}
</Button>
{/if}
<Button
class="wide"
variant={subscription.active ? "secondary" : "default"}
onclick={openPaymentModal}
>
{#if subscription.active}
<RefreshCw size={18} />
{:else if trafficMode}
<Database size={18} />
{/if}
{primaryPayActionLabel()}
</Button>
{#if !subscription.active && appSettings?.trial_enabled && appSettings?.trial_available}
<Button class="wide" variant="secondary" onclick={activateTrial} disabled={trialBusy}>
<Gift size={18} />
{t("wa_activate_trial")}
</Button>
{/if}
{#if canChangeTariff}
<Button class="wide" variant="secondary" onclick={openTariffChangeModal}>
<RefreshCw size={18} />
{t("wa_change_tariff")}
</Button>
{/if}
{#if regularTrafficTopupUnlocked}
<Button class="wide" variant="secondary" onclick={openRegularTopupModal}>
<Database size={18} />
{t("wa_add_traffic")}
</Button>
{/if}
{#if premiumTrafficTopupUnlocked}
<Button class="wide" variant="secondary" onclick={openPremiumTopupModal}>
<Database size={18} />
{t("wa_add_traffic_premium", { target: premiumTitle(subscription) })}
</Button>
{/if}
</div>
</div>
</main>
@@ -0,0 +1,111 @@
<script>
import { Copy, Gift, Ticket, TriangleAlert } from "$components/ui/icons.js";
import { Tooltip } from "$components/ui/primitives.js";
import Button from "$components/ui/button.svelte";
import Card from "$components/ui/card.svelte";
import Input from "$components/ui/input.svelte";
import { StatusMessage } from "$components/patterns/webapp/index.js";
export let referral = {};
export let referralBonusDetails = [];
export let referralOneBonusPerReferee = false;
export let referralWelcomeBonusDays = 0;
export let promoBusy = false;
export let promoCode = "";
export let promoFieldError = "";
export let promoIsError = false;
export let promoStatus = "";
export let applyPromo = () => {};
export let clearPromoFieldError = () => {};
export let copyText = () => {};
export let t = (key) => key;
</script>
<main class="content with-nav">
<Card class="bonus-card">
<div class="bonus-card-head">
<Gift size={42} />
<div>
<strong>{t("wa_referral_bonus_overview_title")}</strong>
{#if referralOneBonusPerReferee}
<p>{t("wa_referral_bonus_once_note")}</p>
{/if}
</div>
</div>
<div>
<h3 class="card-heading">{t("wa_referral_link_title")}</h3>
<div class="copy-row referral-copy-row">
<code>{referral.webapp_link || referral.bot_link || t("wa_link_unavailable")}</code>
<Button
class="referral-copy-button"
onclick={() => copyText(referral.webapp_link || referral.bot_link, t("wa_link_copied"))}
>
{t("wa_copy")}
<Copy size={17} />
</Button>
</div>
</div>
{#if referralBonusDetails.length || referralWelcomeBonusDays > 0}
<div class="referral-bonus-list">
{#if referralWelcomeBonusDays > 0}
<div class="referral-bonus-row">
<strong>{t("wa_referral_bonus_registration_title")}</strong>
<small>{t("wa_referral_bonus_friend_days", { days: referralWelcomeBonusDays })}</small>
</div>
{/if}
{#if referralBonusDetails.length}
<p class="referral-bonus-intro">{t("wa_referral_bonus_paid_intro")}</p>
{/if}
{#each referralBonusDetails as bonus, index (bonus.months || index)}
<div class="referral-bonus-row">
<strong>{bonus.title || `${bonus.months || "?"}`}</strong>
<small
>{t("wa_referral_bonus_you_days", { days: Number(bonus.inviter_days || 0) })}</small
>
<small
>{t("wa_referral_bonus_friend_days", { days: Number(bonus.friend_days || 0) })}</small
>
</div>
{/each}
</div>
{:else}
<StatusMessage>{t("wa_referral_bonus_not_configured")}</StatusMessage>
{/if}
</Card>
<Card>
<h3 class="card-heading card-heading-accent promo-heading">
<Ticket size={18} />
<span>{t("wa_activate_promo_title")}</span>
</h3>
<div class="copy-row">
<div class="field-error-wrap">
<Tooltip.Root open={Boolean(promoFieldError)}>
<Input
bind:value={promoCode}
placeholder="PROMO2026"
class={promoFieldError ? "input-error" : ""}
on:input={clearPromoFieldError}
/>
{#if promoFieldError}
<Tooltip.Trigger class="field-error-trigger" aria-label={promoFieldError}>
<span class="field-error-icon" aria-hidden="true"><TriangleAlert size={18} /></span>
</Tooltip.Trigger>
{/if}
{#if promoFieldError}
<Tooltip.Portal>
<Tooltip.Content class="field-error-tooltip">{promoFieldError}</Tooltip.Content>
</Tooltip.Portal>
{/if}
</Tooltip.Root>
</div>
<Button variant="outline" onclick={applyPromo} disabled={promoBusy}>
{t("wa_activate")}
</Button>
</div>
{#if promoStatus && !(promoIsError && promoFieldError)}
<StatusMessage error={promoIsError}>{promoStatus}</StatusMessage>
{/if}
</Card>
</main>
@@ -0,0 +1,182 @@
<script>
import {
ArrowRight,
CheckCircle2,
FileText,
Mail,
Send,
Shield,
UserRound,
} from "$components/ui/icons.js";
import Button from "$components/ui/button.svelte";
import Card from "$components/ui/card.svelte";
import { LanguageSelect } from "$components/patterns/webapp/index.js";
export let currentLang = "ru";
export let currentLanguageOption = null;
export let emailLinkStatus = "";
export let isAdmin = false;
export let languageBusy = false;
export let languageClickGuard = false;
export let languageClickGuardArmed = false;
export let languageMenuOpen = false;
export let languageOptions = [];
export let linkEmailBusy = false;
export let linkTelegramBusy = false;
export let privacyPolicyUrl = "";
export let profileAvatarUrl = "";
export let profileEmail = "";
export let profileTelegramId = "";
export let supportUrl = "";
export let telegramProfileName = "";
export let user = {};
export let userAgreementUrl = "";
export let userLanguage = "";
export let linkTelegramAccount = () => {};
export let logout = () => {};
export let openAdminPanel = () => {};
export let openExternalLink = () => {};
export let openLinkEmailDialog = () => {};
export let setLanguageMenuOpen = () => {};
export let t = (key) => key;
export let updateAccountLanguage = () => {};
</script>
<main class="content with-nav">
<Card class="settings-profile">
<div class="settings-avatar">
{#if profileAvatarUrl}
<img
src={profileAvatarUrl}
alt={t("wa_settings_avatar_alt")}
loading="lazy"
referrerpolicy="no-referrer"
/>
{:else}
<UserRound size={30} />
{/if}
</div>
<div class="settings-profile-meta">
<strong>{telegramProfileName}</strong>
<small>{profileEmail}</small>
<small>{profileTelegramId}</small>
</div>
</Card>
{#if isAdmin}
<div class="settings-admin-block">
<div class="settings-divider" aria-hidden="true"></div>
<button class="settings-row settings-row-admin" type="button" onclick={openAdminPanel}>
<Shield size={21} />
<span>
<strong>{t("wa_settings_admin_panel", {}, "Админ-панель")}</strong>
<small>{t("wa_settings_admin_panel_hint", {}, "Управление приложением")}</small>
</span>
<ArrowRight size={17} />
</button>
</div>
{/if}
<div class="settings-links-block">
<div class="settings-divider" aria-hidden="true"></div>
{#if user?.telegram_linked}
<div class="settings-row settings-row-linked">
<CheckCircle2 size={21} />
<span>
<strong>{t("wa_settings_telegram_linked_title")}</strong>
<small>{profileTelegramId}</small>
</span>
</div>
{:else}
<Button
variant="telegram"
class="wide settings-telegram-link-btn attention-wrap"
onclick={linkTelegramAccount}
disabled={linkTelegramBusy}
>
<span class="attention-dot" aria-hidden="true"></span>
<Send size={18} />
{t("wa_settings_link_telegram_action")}
</Button>
{/if}
{#if user?.email}
<div class="settings-row settings-row-linked">
<CheckCircle2 size={21} />
<span>
<strong>{t("wa_settings_email_linked_title")}</strong>
<small>{user?.email}</small>
</span>
</div>
{:else}
<button
class="settings-row attention-wrap"
type="button"
onclick={openLinkEmailDialog}
disabled={linkEmailBusy}
>
<span class="attention-dot" aria-hidden="true"></span>
<Mail size={21} />
<span>
<strong>{t("wa_settings_link_email_action")}</strong>
<small>{emailLinkStatus}</small>
</span>
<ArrowRight size={17} />
</button>
{/if}
<div class="settings-divider" aria-hidden="true"></div>
</div>
<div class="settings-list" class:settings-list--language-open={languageMenuOpen}>
<LanguageSelect
bind:open={languageMenuOpen}
value={currentLang}
currentOption={currentLanguageOption}
{userLanguage}
options={languageOptions}
disabled={languageBusy}
clickGuard={languageClickGuard}
clickGuardArmed={languageClickGuardArmed}
closeLabel={t("wa_close")}
label={t("wa_settings_language")}
onOpenChange={setLanguageMenuOpen}
onValueChange={updateAccountLanguage}
/>
{#if supportUrl}
<button
class="settings-row settings-row-support"
type="button"
onclick={() => openExternalLink(supportUrl)}
>
<Send size={21} />
<span><strong>{t("menu_support_button")}</strong></span>
<ArrowRight size={17} />
</button>
{/if}
{#if userAgreementUrl}
<button
class="settings-row settings-row-policy"
type="button"
onclick={() => openExternalLink(userAgreementUrl)}
>
<FileText size={21} />
<span><strong>{t("wa_settings_user_agreement")}</strong></span>
<ArrowRight size={17} />
</button>
{/if}
{#if privacyPolicyUrl}
<button
class="settings-row settings-row-policy"
type="button"
onclick={() => openExternalLink(privacyPolicyUrl)}
>
<Shield size={21} />
<span><strong>{t("wa_settings_privacy_policy")}</strong></span>
<ArrowRight size={17} />
</button>
{/if}
<button class="settings-row settings-row-logout" type="button" onclick={logout}>
<UserRound size={21} />
<span><strong>{t("wa_logout")}</strong><small>{t("wa_end_session")}</small></span>
<ArrowRight size={17} />
</button>
</div>
</main>