This commit is contained in:
talorr
2026-03-27 03:36:08 +03:00
parent 8a97ce6d54
commit cda36918e8
225 changed files with 35641 additions and 0 deletions

134
frontend/pages/index.vue Normal file
View File

@@ -0,0 +1,134 @@
<script setup lang="ts">
import Card from "primevue/card";
import Message from "primevue/message";
import Skeleton from "primevue/skeleton";
import Tag from "primevue/tag";
import type { ActiveSignalCountByBot, Bot } from "~/types";
definePageMeta({
middleware: "auth"
});
type BotCard = {
bot: Bot;
totalSignals: number;
};
const { user } = useAuth();
const loading = ref(true);
const loadError = ref("");
const botCards = ref<BotCard[]>([]);
const availableBots = computed(() => user.value?.botAccesses?.map((access) => access.bot) ?? []);
const formatSignalCount = (count: number) => {
if (count === 0) return "Нет активных сигналов";
if (count === 1) return "1 активный сигнал";
if (count < 5) return `${count} активных сигнала`;
return `${count} активных сигналов`;
};
const loadBotCards = async () => {
loading.value = true;
try {
const response = await useApi<{ items: ActiveSignalCountByBot[] }>("/signals/active-counts");
const countsByBotKey = new Map(response.items.map((item) => [item.botKey, item.activeSignals]));
const cards = availableBots.value.map((bot) => ({
bot,
totalSignals: countsByBotKey.get(bot.key) ?? 0
}));
botCards.value = cards;
loadError.value = "";
} catch (error) {
loadError.value = error instanceof Error ? error.message : "Не удалось загрузить список ботов";
} finally {
loading.value = false;
}
};
onMounted(async () => {
await loadBotCards();
});
watch(
() => availableBots.value.map((bot) => bot.key).join("|"),
(nextKeys, previousKeys) => {
if (nextKeys === previousKeys) return;
void loadBotCards();
}
);
</script>
<template>
<section class="grid gap-4">
<div
class="flex flex-col gap-4 rounded-[24px] border p-5 md:flex-row md:items-start md:justify-between"
:style="{
borderColor: 'var(--border)',
background: 'radial-gradient(circle at top right, color-mix(in srgb, var(--accent) 10%, transparent), transparent 28%), var(--surface)',
boxShadow: '0 10px 30px color-mix(in srgb, var(--text) 4%, transparent)'
}"
>
<div>
<span class="mb-2 inline-block text-xs font-semibold uppercase tracking-[0.18em] text-(--muted)">Доступные боты</span>
<h2 class="m-0 text-3xl font-semibold">Выберите бота</h2>
<p class="m-0 text-sm leading-6 text-(--muted)">
Каждая карточка открывает отдельную ленту сигналов без общего шума и лишних фильтров на старте.
</p>
</div>
<Tag class="rounded" :value="`${availableBots.length} в доступе`" severity="contrast" />
</div>
<Message v-if="loadError" severity="error" :closable="false">{{ loadError }}</Message>
<Message v-else-if="!loading && botCards.length === 0" severity="info" :closable="false">
У пользователя пока нет доступа ни к одному боту.
</Message>
<div v-if="loading" class="bot-grid">
<Card v-for="index in 6" :key="index" class="overflow-hidden rounded-[24px] border shadow-sm">
<template #content>
<div class="grid gap-3 p-4">
<div class="flex items-center justify-between gap-3">
<Skeleton width="2.75rem" height="2.75rem" borderRadius="16px" />
<Skeleton width="2.25rem" height="1.75rem" borderRadius="999px" />
</div>
<Skeleton width="3rem" height="0.7rem" />
<Skeleton width="72%" height="1.45rem" />
<Skeleton width="58%" height="0.95rem" />
<Skeleton width="100%" height="2.5rem" borderRadius="16px" />
</div>
</template>
</Card>
</div>
<div v-else class="bot-grid">
<NuxtLink
v-for="card in botCards"
:key="card.bot.id"
:to="`/bots/${card.bot.key}`"
class="bot-tile"
>
<div class="bot-tile__top">
<div class="bot-tile__icon">
<AppLogo size="26px" />
</div>
<Tag class="bot-tile__count" :value="String(card.totalSignals)" severity="contrast" />
</div>
<div class="bot-tile__body">
<p class="bot-tile__eyebrow">Бот</p>
<h2>{{ card.bot.name }}</h2>
<p class="bot-tile__meta">{{ formatSignalCount(card.totalSignals) }}</p>
</div>
<div class="bot-tile__footer">
<span class="bot-tile__action">Открыть ленту</span>
<i class="pi pi-arrow-right bot-tile__arrow" />
</div>
</NuxtLink>
</div>
</section>
</template>