Files
remnawave-minishop/frontend/src/webapp/auth/AuthScreen.svelte
T

354 lines
13 KiB
Svelte

<script>
import {
Check,
ChevronsUpDown,
Globe2,
LockKeyhole,
Mail,
Send,
TriangleAlert,
} from "$components/ui/icons.js";
import { Select, Tooltip } from "$components/ui/primitives.js";
import Button from "$components/ui/button.svelte";
import BrandMark from "$lib/webapp/BrandMark.svelte";
import EmailCodeScreen from "./EmailCodeScreen.svelte";
import Input from "$components/ui/input.svelte";
import Spinner from "$components/ui/spinner.svelte";
import { StatusMessage } from "$components/patterns/webapp/index.js";
export let screen;
export let CFG;
export let brand = {};
export let brandTitle;
export let email;
export let emailPassword;
export let emailCode;
export let pendingEmail;
export let authStatus;
export let authIsError;
export let authBusy;
export let authResendCooldown;
export let loginEmailFieldError;
export let loginEmailTooltipOpen;
export let passwordLoginFallback;
export let passwordLoginMode;
export let telegramLoginBusy;
export let telegramLoginUnavailable;
export let telegramLoginChecking;
export let telegramLoginLabel;
export let telegramLoginUnavailableMessage;
export let privacyPolicyUrl;
export let userAgreementUrl;
export let currentLang = "ru";
export let currentLanguageOption = null;
export let languageOptions = [];
export let languageMenuOpen = false;
export let languageClickGuard = false;
export let languageClickGuardArmed = false;
export let t;
export let setLanguageMenuOpen = () => {};
export let updateLoginLanguage = () => {};
export let requestEmailCode;
export let loginWithEmailPassword;
export let verifyEmailCode;
export let openTelegramLogin;
export let openExternalLink;
export let submitEmailOnEnter;
export let onBackToLogin;
export let clearLoginEmailError;
export let setPasswordLoginMode;
let authPanelHeight = 0;
$: emailAuthEnabled = CFG.emailAuthEnabled !== false;
$: passwordModeActive = Boolean(passwordLoginMode && emailAuthEnabled);
$: authCardHeight = authPanelHeight ? `${authPanelHeight}px` : undefined;
$: showLanguageSelect = languageOptions.length > 1;
function closeLanguageFromGuard(event) {
event.preventDefault();
event.stopPropagation();
if (languageClickGuardArmed) setLanguageMenuOpen(false);
}
</script>
{#if screen === "code"}
<EmailCodeScreen
bind:code={emailCode}
email={pendingEmail}
busy={authBusy}
resendCooldown={authResendCooldown}
status={authStatus}
isError={authIsError}
{t}
onBack={onBackToLogin}
onConfirm={verifyEmailCode}
onResend={requestEmailCode}
/>
{:else}
<div class="phone-screen auth-screen">
<div class="auth-card-wrap">
<div class="login-brand login-brand-auth">
<BrandMark {brand} size="xl" />
<h1>{brandTitle}</h1>
</div>
<section class="card auth-card" style:height={authCardHeight}>
{#key passwordModeActive}
<div
class={`auth-mode-panel${passwordModeActive ? " auth-mode-panel-password" : ""}`}
bind:clientHeight={authPanelHeight}
>
{#if passwordModeActive}
<div class="auth-pane">
<div class="auth-email-stack">
<div class="field-error-wrap">
<Tooltip.Root open={Boolean(loginEmailFieldError) && loginEmailTooltipOpen}>
<Input
bind:value={email}
type="email"
placeholder={t("wa_email_placeholder")}
autocomplete="email"
class={loginEmailFieldError ? "input-error" : ""}
on:input={clearLoginEmailError}
/>
{#if loginEmailFieldError}
<Tooltip.Trigger
class="field-error-trigger"
aria-label={loginEmailFieldError}
>
<span class="field-error-icon" aria-hidden="true"
><TriangleAlert size={18} /></span
>
</Tooltip.Trigger>
{/if}
{#if loginEmailFieldError}
<Tooltip.Portal>
<Tooltip.Content class="field-error-tooltip"
>{loginEmailFieldError}</Tooltip.Content
>
</Tooltip.Portal>
{/if}
</Tooltip.Root>
</div>
<Input
bind:value={emailPassword}
type="password"
placeholder={t("wa_password_placeholder")}
autocomplete="current-password"
on:keydown={(event) => {
if (event.key !== "Enter") return;
event.preventDefault();
loginWithEmailPassword();
}}
/>
<Button class="wide" onclick={loginWithEmailPassword} disabled={authBusy}>
<LockKeyhole size={18} />
{t("wa_login_password_submit")}
</Button>
{#if passwordLoginFallback}
<button
class="link-button auth-code-fallback"
type="button"
onclick={requestEmailCode}
disabled={authBusy}
>
<Mail size={15} />
{t("wa_login_use_email_code")}
</button>
{:else}
<button
class="link-button auth-code-fallback"
type="button"
onclick={() => setPasswordLoginMode(false)}
disabled={authBusy}
>
{t("wa_login_use_email_code")}
</button>
{/if}
</div>
</div>
{#if authStatus}
<StatusMessage error={authIsError} class="auth-login-status">
{authStatus}
</StatusMessage>
{/if}
{:else}
{#if emailAuthEnabled}
<div class="auth-pane">
<div class="auth-email-stack">
<div class="field-error-wrap">
<Tooltip.Root open={Boolean(loginEmailFieldError) && loginEmailTooltipOpen}>
<Input
bind:value={email}
type="email"
placeholder={t("wa_email_placeholder")}
autocomplete="email"
class={loginEmailFieldError ? "input-error" : ""}
on:keydown={submitEmailOnEnter}
on:input={clearLoginEmailError}
/>
{#if loginEmailFieldError}
<Tooltip.Trigger
class="field-error-trigger"
aria-label={loginEmailFieldError}
>
<span class="field-error-icon" aria-hidden="true"
><TriangleAlert size={18} /></span
>
</Tooltip.Trigger>
{/if}
{#if loginEmailFieldError}
<Tooltip.Portal>
<Tooltip.Content class="field-error-tooltip"
>{loginEmailFieldError}</Tooltip.Content
>
</Tooltip.Portal>
{/if}
</Tooltip.Root>
</div>
<Button class="wide" onclick={requestEmailCode} disabled={authBusy}>
<Mail size={18} />
{t("wa_send_code_email")}
</Button>
</div>
</div>
<div class="or-line"><span></span>{t("wa_or")}<span></span></div>
{/if}
<div class="auth-pane">
<Button
variant="telegram"
class={`wide telegram-login-button${telegramLoginUnavailable ? " unavailable" : ""}${telegramLoginChecking ? " checking" : ""}`}
onclick={openTelegramLogin}
disabled={authBusy || telegramLoginBusy || telegramLoginUnavailable}
aria-label={telegramLoginLabel}
>
<span class="telegram-login-text">
{#if telegramLoginChecking}
<Spinner size="sm" />
{:else}
<Send size={17} />
{/if}
{telegramLoginLabel}
</span>
</Button>
</div>
{#if emailAuthEnabled}
<div class="password-switch-stack">
<div class="password-switch-divider" aria-hidden="true"></div>
<button
class="link-button password-switch-button"
type="button"
onclick={() => setPasswordLoginMode(true)}
disabled={authBusy}
>
<LockKeyhole size={15} />
{t("wa_login_use_password")}
</button>
</div>
{/if}
{#if !telegramLoginChecking && (authStatus || telegramLoginUnavailableMessage)}
<StatusMessage
error={authIsError || Boolean(telegramLoginUnavailableMessage)}
class="auth-login-status"
>
{authStatus || telegramLoginUnavailableMessage}
</StatusMessage>
{/if}
{/if}
</div>
{/key}
</section>
{#if userAgreementUrl || privacyPolicyUrl || showLanguageSelect}
<div class="auth-legal">
{#if userAgreementUrl || privacyPolicyUrl}
<span class="auth-legal-intro">{t("wa_auth_legal_intro")}</span>
<div class="auth-legal-links">
{#if privacyPolicyUrl}
<a
href={privacyPolicyUrl}
target="_blank"
rel="noopener noreferrer"
onclick={(e) => {
e.preventDefault();
openExternalLink(privacyPolicyUrl);
}}
>
{t("wa_auth_legal_privacy")}
</a>
{/if}
{#if privacyPolicyUrl && userAgreementUrl}
<span>{t("wa_auth_legal_and")}</span>
{/if}
{#if userAgreementUrl}
<a
href={userAgreementUrl}
target="_blank"
rel="noopener noreferrer"
onclick={(e) => {
e.preventDefault();
openExternalLink(userAgreementUrl);
}}
>
{t("wa_auth_legal_agreement")}
</a>
{/if}
</div>
{/if}
{#if showLanguageSelect}
{#if languageMenuOpen || languageClickGuard}
<button
class="language-select-guard"
class:language-select-guard--armed={languageClickGuardArmed}
type="button"
aria-label={t("wa_close")}
onpointerdown={closeLanguageFromGuard}
onclick={closeLanguageFromGuard}
></button>
{/if}
<Select.Root
type="single"
bind:open={languageMenuOpen}
value={currentLang}
items={languageOptions}
onOpenChange={setLanguageMenuOpen}
onValueChange={updateLoginLanguage}
>
<Select.Trigger class="auth-language-trigger" aria-label={t("wa_settings_language")}>
<Globe2 size={13} />
<span class="emoji-flag" aria-hidden="true"
>{currentLanguageOption?.flag || "🏳️"}</span
>
<span>{currentLanguageOption?.label || currentLang}</span>
<ChevronsUpDown size={12} />
</Select.Trigger>
<Select.Content
class="language-select-content auth-language-content"
side="bottom"
align="center"
sideOffset={7}
>
<Select.Viewport class="language-select-viewport">
{#each languageOptions as option (option.value)}
<Select.Item
value={option.value}
label={option.label}
class="language-select-item"
>
<span class="language-select-item-main">
<span class="emoji-flag" aria-hidden="true">{option.flag}</span>
<span>{option.label}</span>
</span>
<Check size={15} class="language-select-item-check" />
</Select.Item>
{/each}
</Select.Viewport>
</Select.Content>
</Select.Root>
{/if}
</div>
{/if}
</div>
</div>
{/if}