Redesign messenger connection settings

This commit is contained in:
Ruslan Bakiev
2026-04-06 15:51:10 +07:00
parent 824065f852
commit 17b5a87699
6 changed files with 293 additions and 82 deletions

View File

@@ -1,6 +1,7 @@
<script setup lang="ts">
import { useQuery } from '@vue/apollo-composable';
import { useMutation, useQuery } from '@vue/apollo-composable';
import {
DeleteMyMessengerConnectionDocument,
MeDocument,
MyMessengerConnectionsDocument,
} from '~/composables/graphql/generated';
@@ -12,9 +13,11 @@ import {
} from '~/composables/useMessengerConnectionPresentation';
import { useMessengerStart } from '~/composables/useMessengerStart';
type MessengerChannel = 'TELEGRAM' | 'MAX';
type MessengerItem = {
id: string;
type: 'TELEGRAM' | 'MAX';
type: MessengerChannel;
isActive: boolean;
channelId: string;
displayName?: string | null;
@@ -22,23 +25,56 @@ type MessengerItem = {
avatarAvailable?: boolean | null;
};
type MessengerOption = {
channel: MessengerChannel;
label: string;
title: string;
description: string;
buttonClass: string;
iconClass: string;
unavailableText: string;
};
const config = useRuntimeConfig();
const feedback = ref('');
const meQuery = useQuery(MeDocument);
const connectionsQuery = useQuery(MyMessengerConnectionsDocument);
const deleteConnectionMutation = useMutation(DeleteMyMessengerConnectionDocument);
const { openMessengerBot, pendingChannel } = useMessengerStart();
const telegramConnection = computed(() =>
connectionsQuery.result.value?.myMessengerConnections?.find(
(item: MessengerItem) => item.type === 'TELEGRAM' && item.isActive,
),
) ?? null,
);
const maxConnection = computed(() =>
connectionsQuery.result.value?.myMessengerConnections?.find(
(item: MessengerItem) => item.type === 'MAX' && item.isActive,
),
) ?? null,
);
const messengerOptions: MessengerOption[] = [
{
channel: 'TELEGRAM',
label: 'Telegram',
title: 'Подключить Telegram',
description: 'Получайте статусы заказов и сервисные уведомления в Telegram.',
buttonClass: 'bg-[#1a9c63] text-white hover:bg-[#148553]',
iconClass: 'bg-[#123824] text-white',
unavailableText: 'Telegram пока не настроен в окружении фронта.',
},
{
channel: 'MAX',
label: 'MAX',
title: 'Подключить MAX',
description: 'Открывает MAX-бота и привязывает аккаунт к личному кабинету.',
buttonClass: 'bg-[#2b7fff] text-white hover:bg-[#1d6df1]',
iconClass: 'bg-[#2b7fff] text-white',
unavailableText: 'MAX пока не настроен в окружении фронта.',
},
];
function buildBotConnectUrl(baseUrl: string) {
const accountEmail = meQuery.result.value?.me?.email?.trim().toLowerCase();
if (!accountEmail || !baseUrl) {
@@ -51,9 +87,32 @@ function buildBotConnectUrl(baseUrl: string) {
const telegramConnectUrl = computed(() => buildBotConnectUrl(config.public.telegramBotUrl || ''));
const maxConnectUrl = computed(() => buildBotConnectUrl(config.public.maxBotUrl || ''));
async function connectMessenger(channel: 'TELEGRAM' | 'MAX') {
const baseUrl = channel === 'TELEGRAM' ? telegramConnectUrl.value : maxConnectUrl.value;
function connectUrl(channel: MessengerChannel) {
return channel === 'TELEGRAM' ? telegramConnectUrl.value : maxConnectUrl.value;
}
function connectionFor(channel: MessengerChannel) {
return channel === 'TELEGRAM' ? telegramConnection.value : maxConnection.value;
}
const activeConnections = computed(() => messengerOptions
.map((option) => ({
option,
connection: connectionFor(option.channel),
}))
.filter((item) => Boolean(item.connection)));
const availableOptions = computed(() => messengerOptions
.filter((option) => !connectionFor(option.channel)));
async function connectMessenger(channel: MessengerChannel) {
feedback.value = '';
const baseUrl = connectUrl(channel);
if (!baseUrl) {
feedback.value = channel === 'MAX'
? 'MAX не откроется, пока не задан NUXT_PUBLIC_MAX_BOT_URL.'
: 'Telegram не откроется, пока не задан NUXT_PUBLIC_TELEGRAM_BOT_URL.';
return;
}
@@ -63,92 +122,189 @@ async function connectMessenger(channel: 'TELEGRAM' | 'MAX') {
redirectPath: `/profile/notifications/success?connected=${channel.toLowerCase()}`,
});
}
async function removeConnection(connectionId: string) {
feedback.value = '';
const result = await deleteConnectionMutation.mutate({
connectionId,
});
if (!result?.data?.deleteMyMessengerConnection) {
feedback.value = 'Не удалось отключить аккаунт. Попробуйте еще раз.';
return;
}
await connectionsQuery.refetch();
}
</script>
<template>
<section class="space-y-6">
<NuxtLink to="/profile" class="link link-hover text-sm"> Назад в профиль</NuxtLink>
<h1 class="text-3xl font-extrabold text-[#0f2f20]">Уведомления</h1>
<div class="surface-card rounded-3xl p-5">
<p class="text-sm text-[#355947]">
Подключите Telegram и Max, чтобы получать статусы заказов и важные уведомления в удобном канале.
<div class="space-y-2">
<h1 class="text-3xl font-extrabold text-[#0f2f20]">Уведомления</h1>
<p class="max-w-3xl text-sm leading-6 text-[#466653]">
Подключите удобные мессенджеры, чтобы получать статусы заказов и важные уведомления без лишних переходов в кабинет.
</p>
</div>
<div class="mt-4 space-y-3">
<div class="rounded-2xl bg-[#f8fbf9] p-4 transition hover:shadow-md">
<p class="font-semibold">Telegram</p>
<div v-if="telegramConnection" class="mt-3 flex items-center gap-3 rounded-2xl bg-white px-3 py-2">
<div v-if="messengerConnectionAvatarSrc(telegramConnection)" class="avatar">
<div class="h-11 w-11 rounded-full">
<img :src="messengerConnectionAvatarSrc(telegramConnection)" :alt="messengerConnectionName(telegramConnection)">
</div>
<div
v-if="feedback"
class="rounded-[24px] border px-4 py-3 text-sm font-medium"
:class="feedback.includes('Не удалось') || feedback.includes('не откроется')
? 'border-[#f1d1c7] bg-[#fff3ef] text-[#9d4426]'
: 'border-[#cbe9d6] bg-[#f1fbf5] text-[#1c6b45]'"
>
{{ feedback }}
</div>
<div
v-if="activeConnections.length === 0"
class="rounded-[32px] bg-[#edf3ee] p-6 md:p-8"
>
<div class="space-y-3">
<h2 class="text-2xl font-black tracking-[-0.03em] text-[#123824]">Подключите мессенджеры</h2>
<p class="max-w-3xl text-sm leading-6 text-[#557562]">
Вы можете подключить любой из мессенджеров ниже. После подключения уведомления о заказах и важных действиях будут приходить прямо туда.
</p>
</div>
<div class="mt-6 grid gap-4 md:grid-cols-2">
<button
v-for="option in messengerOptions"
:key="option.channel"
class="flex min-h-[120px] flex-col items-start justify-between rounded-[28px] border-0 px-5 py-5 text-left shadow-[0_18px_38px_rgba(18,56,36,0.08)] transition"
:class="[option.buttonClass, { 'opacity-60': !connectUrl(option.channel) }]"
:disabled="pendingChannel === option.channel || !connectUrl(option.channel)"
@click="connectMessenger(option.channel)"
>
<div class="space-y-2">
<div class="inline-flex h-11 w-11 items-center justify-center rounded-2xl bg-white/18 text-sm font-black text-white">
{{ option.channel === 'TELEGRAM' ? 'TG' : 'MX' }}
</div>
<div v-else class="avatar placeholder">
<div class="h-11 w-11 rounded-full bg-[#123824] text-sm font-bold text-white">
<span>{{ messengerConnectionInitials(telegramConnection, 'TG') }}</span>
</div>
</div>
<div class="min-w-0">
<p class="truncate text-sm font-semibold text-[#123824]">
{{ messengerConnectionName(telegramConnection) }}
</p>
<p class="truncate text-xs text-[#5c7b69]">
{{ messengerConnectionHandle(telegramConnection) || 'Подключен' }}
</p>
<div>
<p class="text-lg font-black tracking-[-0.03em]">{{ option.title }}</p>
<p class="mt-1 text-sm text-white/82">{{ option.description }}</p>
</div>
</div>
<p v-else class="text-sm opacity-80">Не подключен</p>
<button
class="btn btn-secondary mt-3 w-full"
:class="{ 'btn-disabled pointer-events-none': !telegramConnectUrl }"
:disabled="pendingChannel === 'TELEGRAM' || !telegramConnectUrl"
@click="connectMessenger('TELEGRAM')"
>
{{
pendingChannel === 'TELEGRAM'
? 'Открываем Telegram…'
: telegramConnection
? 'Переподключить Telegram'
: 'Подключить Telegram'
}}
</button>
</div>
<p class="text-sm font-semibold text-white/90">
{{ pendingChannel === option.channel ? `Открываем ${option.label}...` : `Перейти в ${option.label}` }}
</p>
</button>
</div>
<div class="rounded-2xl bg-[#f8fbf9] p-4 transition hover:shadow-md">
<p class="font-semibold">Max</p>
<div v-if="maxConnection" class="mt-3 flex items-center gap-3 rounded-2xl bg-white px-3 py-2">
<div class="avatar placeholder">
<div class="h-11 w-11 rounded-full bg-[#2b7fff] text-sm font-bold text-white">
<span>{{ messengerConnectionInitials(maxConnection, 'MX') }}</span>
</div>
</div>
<div class="min-w-0">
<p class="truncate text-sm font-semibold text-[#123824]">
{{ messengerConnectionName(maxConnection) }}
</p>
<p class="truncate text-xs text-[#5c7b69]">
{{ messengerConnectionHandle(maxConnection) || 'Подключен' }}
</p>
</div>
</div>
<p v-else class="text-sm opacity-80">Не подключен</p>
<button
class="btn btn-accent mt-3 w-full"
:class="{ 'btn-disabled pointer-events-none': !maxConnectUrl }"
:disabled="pendingChannel === 'MAX' || !maxConnectUrl"
@click="connectMessenger('MAX')"
>
{{
pendingChannel === 'MAX'
? 'Открываем Max…'
: maxConnection
? 'Переподключить Max'
: 'Подключить Max'
}}
</button>
</div>
<div class="mt-4 space-y-2">
<p
v-for="option in messengerOptions.filter((item) => !connectUrl(item.channel))"
:key="`${option.channel}-hint`"
class="text-sm text-[#8b5a49]"
>
{{ option.unavailableText }}
</p>
</div>
</div>
<template v-else>
<div class="rounded-[32px] bg-[#edf3ee] p-6 md:p-8">
<div class="space-y-3">
<h2 class="text-2xl font-black tracking-[-0.03em] text-[#123824]">Подключенные аккаунты</h2>
<p class="text-sm leading-6 text-[#557562]">
Здесь показаны активные мессенджеры, привязанные к вашему кабинету.
</p>
</div>
<div class="mt-6 space-y-4">
<article
v-for="{ option, connection } in activeConnections"
:key="connection!.id"
class="rounded-[28px] bg-white px-5 py-4 shadow-[0_18px_38px_rgba(18,56,36,0.08)]"
>
<div class="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
<div class="flex min-w-0 items-center gap-4">
<div v-if="messengerConnectionAvatarSrc(connection)" class="avatar">
<div class="h-14 w-14 rounded-[20px]">
<img :src="messengerConnectionAvatarSrc(connection)" :alt="messengerConnectionName(connection)">
</div>
</div>
<div
v-else
class="flex h-14 w-14 items-center justify-center rounded-[20px] text-sm font-black"
:class="option.iconClass"
>
{{ messengerConnectionInitials(connection, option.channel === 'TELEGRAM' ? 'TG' : 'MX') }}
</div>
<div class="min-w-0">
<p class="text-[11px] font-semibold uppercase tracking-[0.18em] text-[#6a8a76]">{{ option.label }}</p>
<p class="truncate text-lg font-bold text-[#123824]">{{ messengerConnectionName(connection) }}</p>
<p class="truncate text-sm text-[#557562]">{{ messengerConnectionHandle(connection) || connection.channelId }}</p>
</div>
</div>
<button
class="btn rounded-full border border-[#e5cfc7] bg-[#fff5f1] px-5 text-[#a64d2d] hover:border-[#deb5a8] hover:bg-[#ffe8e0]"
:disabled="deleteConnectionMutation.loading.value"
@click="removeConnection(connection!.id)"
>
{{ deleteConnectionMutation.loading.value ? 'Удаляем...' : 'Удалить' }}
</button>
</div>
</article>
</div>
</div>
<div
v-if="availableOptions.length > 0"
class="rounded-[32px] bg-[#edf3ee] p-6 md:p-8"
>
<div class="space-y-3">
<h2 class="text-2xl font-black tracking-[-0.03em] text-[#123824]">Можно подключить еще</h2>
<p class="text-sm leading-6 text-[#557562]">
Добавьте второй канал, чтобы не потерять уведомления, если один мессенджер недоступен.
</p>
</div>
<div class="mt-6 grid gap-4 md:grid-cols-2">
<button
v-for="option in availableOptions"
:key="option.channel"
class="flex min-h-[112px] flex-col items-start justify-between rounded-[28px] border-0 px-5 py-5 text-left shadow-[0_18px_38px_rgba(18,56,36,0.08)] transition"
:class="[option.buttonClass, { 'opacity-60': !connectUrl(option.channel) }]"
:disabled="pendingChannel === option.channel || !connectUrl(option.channel)"
@click="connectMessenger(option.channel)"
>
<div>
<p class="text-lg font-black tracking-[-0.03em]">{{ option.title }}</p>
<p class="mt-2 text-sm text-white/82">{{ option.description }}</p>
</div>
<p class="text-sm font-semibold text-white/90">
{{ pendingChannel === option.channel ? `Открываем ${option.label}...` : `Подключить ${option.label}` }}
</p>
</button>
</div>
<div class="mt-4 space-y-2">
<p
v-for="option in availableOptions.filter((item) => !connectUrl(item.channel))"
:key="`${option.channel}-connected-hint`"
class="text-sm text-[#8b5a49]"
>
{{ option.unavailableText }}
</p>
</div>
</div>
<div
v-else
class="rounded-[32px] bg-[linear-gradient(135deg,#123824_0%,#1a5635_100%)] p-6 text-white md:p-8"
>
<p class="text-[11px] font-semibold uppercase tracking-[0.18em] text-white/65">Готово</p>
<h2 class="mt-3 text-2xl font-black tracking-[-0.03em]">Оба канала подключены</h2>
<p class="mt-2 max-w-3xl text-sm leading-6 text-white/78">
Теперь важные уведомления будут доступны и в Telegram, и в MAX. Если захотите сменить аккаунт, удалите текущий и подключите новый.
</p>
</div>
</template>
</section>
</template>