Simplify login screen
This commit is contained in:
39
app/app.vue
39
app/app.vue
@@ -96,16 +96,39 @@ const managerPageTabs = computed(() => {
|
|||||||
|
|
||||||
return [];
|
return [];
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const mainClass = computed(() => {
|
||||||
|
if (isBonusProgramPage.value) {
|
||||||
|
return 'bonus-program-main';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLoginPage.value) {
|
||||||
|
return 'mx-auto flex min-h-screen w-full max-w-[1440px] items-center justify-center p-4 md:p-6 lg:p-8';
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'mx-auto w-full max-w-[1440px] p-4 pt-[104px] md:p-6 md:pt-[112px] lg:p-8 lg:pt-[118px]',
|
||||||
|
hasManagerDock.value ? 'pb-[116px] md:pb-[128px]' : '',
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
const pageFrameClass = computed(() => {
|
||||||
|
if (isBonusProgramPage.value) {
|
||||||
|
return 'bonus-program-stage';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLoginPage.value) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return ['lk-content-canvas', { 'lk-content-canvas--with-tabs': managerPageTabs.value.length }];
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div :class="isBonusProgramPage ? 'bonus-program-shell' : 'lk-shell'" data-theme="aqua">
|
<div :class="isBonusProgramPage ? 'bonus-program-shell' : 'lk-shell'" data-theme="aqua">
|
||||||
<UiAppHeader v-if="!isLoginPage && !isBonusProgramPage" />
|
<UiAppHeader v-if="!isLoginPage && !isBonusProgramPage" />
|
||||||
<main
|
<main :class="mainClass">
|
||||||
:class="isBonusProgramPage
|
|
||||||
? 'bonus-program-main'
|
|
||||||
: ['mx-auto w-full max-w-[1440px] p-4 pt-[104px] md:p-6 md:pt-[112px] lg:p-8 lg:pt-[118px]', hasManagerDock ? 'pb-[116px] md:pb-[128px]' : '']"
|
|
||||||
>
|
|
||||||
<div v-if="managerPageTabs.length && !isBonusProgramPage" class="lk-page-tabs-shell">
|
<div v-if="managerPageTabs.length && !isBonusProgramPage" class="lk-page-tabs-shell">
|
||||||
<nav class="manager-page-tabs" aria-label="Разделы страницы">
|
<nav class="manager-page-tabs" aria-label="Разделы страницы">
|
||||||
<NuxtLink
|
<NuxtLink
|
||||||
@@ -120,11 +143,7 @@ const managerPageTabs = computed(() => {
|
|||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div :class="pageFrameClass">
|
||||||
:class="isBonusProgramPage
|
|
||||||
? 'bonus-program-stage'
|
|
||||||
: ['lk-content-canvas', { 'lk-content-canvas--with-tabs': managerPageTabs.length }]"
|
|
||||||
>
|
|
||||||
<NuxtPage />
|
<NuxtPage />
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -24,8 +24,6 @@ const expiresAt = ref('');
|
|||||||
const code = ref('');
|
const code = ref('');
|
||||||
const feedback = ref('');
|
const feedback = ref('');
|
||||||
const feedbackTone = ref<'success' | 'error'>('success');
|
const feedbackTone = ref<'success' | 'error'>('success');
|
||||||
const autoRequestTimer = ref<ReturnType<typeof setTimeout> | null>(null);
|
|
||||||
const lastRequestedEmail = ref('');
|
|
||||||
|
|
||||||
const requestCodeMutation = useMutation(RequestLoginCodeDocument, { throws: 'never' });
|
const requestCodeMutation = useMutation(RequestLoginCodeDocument, { throws: 'never' });
|
||||||
const verifyCodeMutation = useMutation(VerifyLoginCodeDocument, { throws: 'never' });
|
const verifyCodeMutation = useMutation(VerifyLoginCodeDocument, { throws: 'never' });
|
||||||
@@ -44,6 +42,9 @@ const maxBotUrl = computed(() => config.public.maxBotUrl || '');
|
|||||||
|
|
||||||
const normalizedEmail = computed(() => email.value.trim().toLowerCase());
|
const normalizedEmail = computed(() => email.value.trim().toLowerCase());
|
||||||
const isEmailReady = computed(() => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(normalizedEmail.value));
|
const isEmailReady = computed(() => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(normalizedEmail.value));
|
||||||
|
const canUseTelegramLogin = computed(() => messengerMiniAppChannel.value !== 'TELEGRAM' && Boolean(telegramBotUrl.value));
|
||||||
|
const canUseMaxLogin = computed(() => messengerMiniAppChannel.value !== 'MAX' && Boolean(maxBotUrl.value));
|
||||||
|
const hasMessengerButtons = computed(() => canUseTelegramLogin.value || canUseMaxLogin.value);
|
||||||
const nextPath = computed(() =>
|
const nextPath = computed(() =>
|
||||||
typeof route.query.next === 'string' && route.query.next.startsWith('/')
|
typeof route.query.next === 'string' && route.query.next.startsWith('/')
|
||||||
? route.query.next
|
? route.query.next
|
||||||
@@ -69,15 +70,6 @@ async function navigateAfterLogin(user: { company?: { id: string } | null; compa
|
|||||||
await navigateTo('/');
|
await navigateTo('/');
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearAutoRequestTimer() {
|
|
||||||
if (!autoRequestTimer.value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
clearTimeout(autoRequestTimer.value);
|
|
||||||
autoRequestTimer.value = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeApolloErrorMessage(message: string) {
|
function normalizeApolloErrorMessage(message: string) {
|
||||||
if (message.includes('User for this destination was not found.')) {
|
if (message.includes('User for this destination was not found.')) {
|
||||||
return 'Пользователь с таким e-mail не найден. Вход доступен только для созданных аккаунтов.';
|
return 'Пользователь с таким e-mail не найден. Вход доступен только для созданных аккаунтов.';
|
||||||
@@ -118,7 +110,6 @@ async function requestCode() {
|
|||||||
if (requestCodeMutation.loading.value) {
|
if (requestCodeMutation.loading.value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
clearAutoRequestTimer();
|
|
||||||
|
|
||||||
feedback.value = '';
|
feedback.value = '';
|
||||||
const result = await requestCodeMutation.mutate({
|
const result = await requestCodeMutation.mutate({
|
||||||
@@ -135,7 +126,6 @@ async function requestCode() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
lastRequestedEmail.value = normalizedEmail.value;
|
|
||||||
challengeToken.value = payload.challengeToken;
|
challengeToken.value = payload.challengeToken;
|
||||||
maskedEmail.value = payload.destination;
|
maskedEmail.value = payload.destination;
|
||||||
expiresAt.value = new Date(payload.expiresAt).toLocaleString();
|
expiresAt.value = new Date(payload.expiresAt).toLocaleString();
|
||||||
@@ -172,6 +162,15 @@ async function verifyCode() {
|
|||||||
await navigateAfterLogin(payload.user);
|
await navigateAfterLogin(payload.user);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function returnToRequestStep() {
|
||||||
|
step.value = 'request';
|
||||||
|
code.value = '';
|
||||||
|
feedback.value = '';
|
||||||
|
challengeToken.value = '';
|
||||||
|
maskedEmail.value = '';
|
||||||
|
expiresAt.value = '';
|
||||||
|
}
|
||||||
|
|
||||||
async function consumeLoginToken(loginToken: string) {
|
async function consumeLoginToken(loginToken: string) {
|
||||||
feedback.value = '';
|
feedback.value = '';
|
||||||
const result = await consumeLoginTokenMutation.mutate({
|
const result = await consumeLoginTokenMutation.mutate({
|
||||||
@@ -294,47 +293,6 @@ async function startMessengerLogin(channel: 'TELEGRAM' | 'MAX') {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function scheduleAutoRequest() {
|
|
||||||
clearAutoRequestTimer();
|
|
||||||
|
|
||||||
if (step.value !== 'request') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!isEmailReady.value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (normalizedEmail.value === lastRequestedEmail.value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
autoRequestTimer.value = setTimeout(() => {
|
|
||||||
void requestCode();
|
|
||||||
}, 450);
|
|
||||||
}
|
|
||||||
|
|
||||||
function onEmailBlur() {
|
|
||||||
if (step.value !== 'request') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!isEmailReady.value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (normalizedEmail.value === lastRequestedEmail.value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
void requestCode();
|
|
||||||
}
|
|
||||||
|
|
||||||
watch([normalizedEmail, step], () => {
|
|
||||||
if (step.value !== 'request') {
|
|
||||||
clearAutoRequestTimer();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
scheduleAutoRequest();
|
|
||||||
});
|
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
const loginToken = typeof route.query.login_token === 'string' ? route.query.login_token : '';
|
const loginToken = typeof route.query.login_token === 'string' ? route.query.login_token : '';
|
||||||
if (loginToken) {
|
if (loginToken) {
|
||||||
@@ -344,112 +302,133 @@ onMounted(async () => {
|
|||||||
|
|
||||||
await tryMessengerMiniAppLogin();
|
await tryMessengerMiniAppLogin();
|
||||||
});
|
});
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
|
||||||
clearAutoRequestTimer();
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section class="mx-auto flex min-h-[calc(100vh-4rem)] w-full max-w-3xl items-center py-8">
|
<section class="mx-auto flex w-full max-w-[540px] items-center justify-center py-6 md:py-10">
|
||||||
<div class="card w-full border border-base-300/60 bg-base-100 shadow-xl">
|
<div class="surface-card w-full rounded-[32px] border border-white/70 px-6 py-6 shadow-[0_26px_70px_rgba(18,56,36,0.12)] md:px-8 md:py-8">
|
||||||
<div class="card-body p-5 md:p-8">
|
<div class="space-y-6">
|
||||||
<div class="mb-4 text-center">
|
<div class="space-y-3 text-center">
|
||||||
<h1 class="text-3xl font-extrabold">Вход в личный кабинет</h1>
|
<p class="text-[11px] font-bold uppercase tracking-[0.22em] text-[#6a8a76]">Фрегат</p>
|
||||||
<p
|
<div class="space-y-2">
|
||||||
v-if="telegramMiniAppMode === 'checking'"
|
<h1 class="text-3xl font-black tracking-[-0.04em] text-[#123824] md:text-4xl">Вход</h1>
|
||||||
class="mt-2 text-sm text-base-content/70"
|
<p class="text-sm leading-6 text-[#5c7b69]">
|
||||||
>
|
Войдите по рабочему e-mail и коду из письма.
|
||||||
{{ `Проверяем аккаунт ${resolveMessengerMiniAppLabel()}…` }}
|
|
||||||
</p>
|
</p>
|
||||||
<p
|
</div>
|
||||||
v-else-if="isMessengerMiniApp"
|
</div>
|
||||||
class="mt-2 text-sm text-base-content/70"
|
|
||||||
|
<div
|
||||||
|
v-if="telegramMiniAppMode === 'checking' || isMessengerMiniApp"
|
||||||
|
class="rounded-[24px] border px-4 py-3 text-sm leading-6"
|
||||||
|
:class="telegramMiniAppMode === 'checking'
|
||||||
|
? 'border-[#dce9e1] bg-[#f7fbf9] text-[#355947]'
|
||||||
|
: 'border-[#cbe9d6] bg-[#f1fbf5] text-[#1c6b45]'"
|
||||||
>
|
>
|
||||||
|
<template v-if="telegramMiniAppMode === 'checking'">
|
||||||
|
{{ `Проверяем аккаунт ${resolveMessengerMiniAppLabel()}…` }}
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
{{
|
{{
|
||||||
messengerMiniAppDisplayName
|
messengerMiniAppDisplayName
|
||||||
? `Вы вошли из ${resolveMessengerMiniAppLabel()} как ${messengerMiniAppDisplayName}.`
|
? `Вы вошли из ${resolveMessengerMiniAppLabel()} как ${messengerMiniAppDisplayName}.`
|
||||||
: `Вы открыли кабинет внутри ${resolveMessengerMiniAppLabel()}.`
|
: `Вы открыли кабинет внутри ${resolveMessengerMiniAppLabel()}.`
|
||||||
}}
|
}}
|
||||||
</p>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="step === 'request'" class="space-y-4">
|
<div v-if="step === 'request'" class="space-y-5">
|
||||||
<fieldset class="fieldset">
|
<label class="block space-y-2">
|
||||||
<legend class="fieldset-legend text-base font-semibold">E-mail</legend>
|
<span class="text-sm font-semibold text-[#355947]">E-mail</span>
|
||||||
<input
|
<input
|
||||||
v-model="email"
|
v-model="email"
|
||||||
type="email"
|
type="email"
|
||||||
class="input input-bordered w-full"
|
class="input manager-field h-14 w-full px-4 text-base text-[#123824]"
|
||||||
placeholder="name@company.com"
|
placeholder="name@company.com"
|
||||||
@keydown.enter.prevent="requestCode"
|
@keydown.enter.prevent="requestCode"
|
||||||
@blur="onEmailBlur"
|
|
||||||
>
|
>
|
||||||
</fieldset>
|
</label>
|
||||||
|
|
||||||
<div class="grid gap-2 sm:grid-cols-2">
|
|
||||||
<button
|
<button
|
||||||
v-if="messengerMiniAppChannel !== 'TELEGRAM'"
|
class="btn h-12 w-full rounded-full border-0 bg-[#123824] text-white shadow-[0_16px_32px_rgba(18,56,36,0.18)] hover:bg-[#0f2f20] disabled:border-0 disabled:bg-[#cfd8d2] disabled:text-[#6f8579]"
|
||||||
class="btn btn-secondary"
|
:disabled="requestCodeMutation.loading.value || !isEmailReady"
|
||||||
:class="{ 'btn-disabled pointer-events-none': !telegramBotUrl || !isEmailReady }"
|
@click="requestCode"
|
||||||
:disabled="pendingChannel === 'TELEGRAM' || !telegramBotUrl || !isEmailReady"
|
>
|
||||||
|
{{ requestCodeMutation.loading.value ? 'Отправляем код…' : 'Получить код' }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div v-if="hasMessengerButtons" class="space-y-3">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<span class="h-px flex-1 bg-[#e2ece6]" />
|
||||||
|
<span class="text-[11px] font-bold uppercase tracking-[0.18em] text-[#7a9386]">или войти через</span>
|
||||||
|
<span class="h-px flex-1 bg-[#e2ece6]" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid gap-3 sm:grid-cols-2">
|
||||||
|
<button
|
||||||
|
v-if="canUseTelegramLogin"
|
||||||
|
class="btn h-12 rounded-full border border-[#d7e6dc] bg-white text-[#123824] hover:border-[#bed6c7] hover:bg-[#f6fbf8] disabled:border-[#e6ede9] disabled:bg-[#f8fbf9] disabled:text-[#91a79b]"
|
||||||
|
:disabled="pendingChannel === 'TELEGRAM' || !isEmailReady"
|
||||||
@click="startMessengerLogin('TELEGRAM')"
|
@click="startMessengerLogin('TELEGRAM')"
|
||||||
>
|
>
|
||||||
{{ pendingChannel === 'TELEGRAM' ? 'Открываем Telegram…' : 'Войти через Telegram' }}
|
{{ pendingChannel === 'TELEGRAM' ? 'Открываем Telegram…' : 'Telegram' }}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
v-if="messengerMiniAppChannel !== 'MAX'"
|
v-if="canUseMaxLogin"
|
||||||
class="btn btn-accent"
|
class="btn h-12 rounded-full border border-[#d7e6dc] bg-white text-[#123824] hover:border-[#bed6c7] hover:bg-[#f6fbf8] disabled:border-[#e6ede9] disabled:bg-[#f8fbf9] disabled:text-[#91a79b]"
|
||||||
:class="{ 'btn-disabled pointer-events-none': !maxBotUrl || !isEmailReady }"
|
:disabled="pendingChannel === 'MAX' || !isEmailReady"
|
||||||
:disabled="pendingChannel === 'MAX' || !maxBotUrl || !isEmailReady"
|
|
||||||
@click="startMessengerLogin('MAX')"
|
@click="startMessengerLogin('MAX')"
|
||||||
>
|
>
|
||||||
{{ pendingChannel === 'MAX' ? 'Открываем Max…' : 'Войти через Max' }}
|
{{ pendingChannel === 'MAX' ? 'Открываем Max…' : 'Max' }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<p v-if="requestCodeMutation.loading.value" class="text-sm text-base-content/70">
|
|
||||||
Проверяем e-mail и отправляем код...
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else class="space-y-4">
|
<div v-else class="space-y-5">
|
||||||
<p class="text-sm text-base-content/70">E-mail: {{ maskedEmail }}</p>
|
<div class="rounded-[24px] bg-[#f5faf7] px-4 py-3 text-sm text-[#355947]">
|
||||||
|
Код отправлен на <span class="font-semibold text-[#123824]">{{ maskedEmail }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<fieldset class="fieldset">
|
<label class="block space-y-2">
|
||||||
<legend class="fieldset-legend text-base font-semibold">Код</legend>
|
<span class="text-sm font-semibold text-[#355947]">Код из письма</span>
|
||||||
<input
|
<input
|
||||||
v-model="code"
|
v-model="code"
|
||||||
type="text"
|
type="text"
|
||||||
maxlength="6"
|
maxlength="6"
|
||||||
class="input input-bordered w-full"
|
class="input manager-field h-14 w-full px-4 text-base tracking-[0.22em] text-[#123824]"
|
||||||
placeholder="123456"
|
placeholder="123456"
|
||||||
@keydown.enter.prevent="verifyCode"
|
@keydown.enter.prevent="verifyCode"
|
||||||
>
|
>
|
||||||
</fieldset>
|
</label>
|
||||||
|
|
||||||
|
<div class="space-y-3">
|
||||||
<button
|
<button
|
||||||
class="btn btn-primary w-full"
|
class="btn h-12 w-full rounded-full border-0 bg-[#123824] text-white shadow-[0_16px_32px_rgba(18,56,36,0.18)] hover:bg-[#0f2f20] disabled:border-0 disabled:bg-[#cfd8d2] disabled:text-[#6f8579]"
|
||||||
:disabled="verifyCodeMutation.loading.value"
|
:disabled="verifyCodeMutation.loading.value || !code.trim()"
|
||||||
@click="verifyCode"
|
@click="verifyCode"
|
||||||
>
|
>
|
||||||
{{ verifyCodeMutation.loading.value ? 'Проверяем…' : 'Войти' }}
|
{{ verifyCodeMutation.loading.value ? 'Проверяем…' : 'Войти' }}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="btn btn-ghost w-full"
|
class="btn h-12 w-full rounded-full border border-[#d7e6dc] bg-white text-[#123824] hover:border-[#bed6c7] hover:bg-[#f6fbf8]"
|
||||||
@click="step = 'request'; code = ''; feedback = ''; challengeToken = ''; maskedEmail = ''"
|
@click="returnToRequestStep"
|
||||||
>
|
>
|
||||||
Изменить e-mail
|
Изменить e-mail
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<p class="text-xs text-base-content/60">Код действует до {{ expiresAt }}</p>
|
<p class="text-xs text-[#7a9386]">Код действует до {{ expiresAt }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="feedback"
|
v-if="feedback"
|
||||||
class="alert mt-2"
|
class="rounded-[24px] border px-4 py-3 text-sm leading-6"
|
||||||
:class="feedbackTone === 'success' ? 'alert-success' : 'alert-error'"
|
:class="feedbackTone === 'success'
|
||||||
|
? 'border-[#cbe9d6] bg-[#f1fbf5] text-[#1c6b45]'
|
||||||
|
: 'border-[#f1d1c7] bg-[#fff3ef] text-[#9d4426]'"
|
||||||
>
|
>
|
||||||
{{ feedback }}
|
{{ feedback }}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user