From d03336174f5e075cc5c90f2bfd5edfc7eb18aa95 Mon Sep 17 00:00:00 2001 From: Untone Date: Mon, 7 Jul 2025 17:51:48 +0300 Subject: [PATCH] admin-ui-fix --- CHANGELOG.md | 26 ++++++++ panel/graphql/queries.ts | 5 ++ panel/routes/reactions.tsx | 115 +++++++++++++++++++++++----------- panel/routes/shouts.tsx | 17 ++++- panel/styles/Admin.module.css | 37 ++++++++--- 5 files changed, 153 insertions(+), 47 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b4f71e48..376ac6fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,32 @@ Добавлена полная система просмотра и модерации реакций с расширенными возможностями фильтрации и управления. +#### Улучшения интерфейса фильтрации реакций +- **Упрощена фильтрация по статусу**: Заменен выпадающий список "Все статусы/Активные/Удаленные" на простую галочку "Только удаленные" +- **Цветовой индикатор статуса**: Убрана колонка "Статус", статус теперь отображается цветом фона ID реакции +- **Цветовая схема**: Зеленый фон (#d1fae5) для активных реакций, красный фон (#fee2e2) для удаленных +- **Tooltip статуса**: При наведении на ID показывается текстовое описание статуса ("Активна" / "Удалена") +- **Перераспределение колонок**: Увеличена ширина колонок "Текст" (28%), "Автор" (20%) и "Публикация" (25%) за счет убранной колонки статуса +- **Улучшенные стили**: Добавлены стили для галочки с hover эффектами и правильным позиционированием + +#### Расширенная информация об авторах в tooltip'ах +- **Дата регистрации в tooltip'ах**: Во всех таблицах админ-панели (публикации и реакции) tooltip'ы авторов теперь показывают не только email, но и дату регистрации с предлогом "с" +- **Формат tooltip'а**: "email@example.com с 01.10.2023" - краткий и информативный формат +- **GraphQL обновления**: Добавлено поле `created_at` для всех полей авторов в запросах `ADMIN_GET_SHOUTS_QUERY` и `ADMIN_GET_REACTIONS_QUERY` +- **Безопасная типизация**: Функция `formatAuthorTooltip()` корректно обрабатывает отсутствующие поля и возвращает fallback значения +- **Локализация**: Дата форматируется в русском формате (ДД.ММ.ГГГГ) через `toLocaleDateString('ru-RU')` + +#### Улучшенный поиск и автоматическая фильтрация +- **Умный поиск по ID публикаций**: Строка поиска теперь автоматически определяет числовые запросы как ID публикаций и ищет реакции к конкретной публикации +- **Расширенный placeholder**: "Поиск по тексту, автору, публикации или ID публикации..." - информирует о всех возможностях поиска +- **Автоматическое применение фильтров**: Убрана кнопка "Применить фильтры" - фильтры применяются мгновенно при изменении: + - Галочка "Только удаленные" срабатывает сразу при клике + - Выбор типа реакции (лайк, комментарий и т.д.) применяется автоматически + - Поиск запускается при каждом изменении строки поиска +- **Убрано отдельное поле ID**: Удалено дублирующее поле "ID публикации" - теперь поиск по ID происходит через основную строку поиска +- **Оптимизированная логика**: Использование `createEffect` для отслеживания изменений всех фильтров без дублирования запросов +- **Улучшенный UX**: Более быстрый и интуитивный интерфейс без лишних кнопок и полей + #### Новая функциональность - **Вкладка "Реакции"** в навигации админ-панели с эмоджи-индикаторами - **Просмотр всех реакций** с детальной информацией о типе, авторе, публикации и статистике diff --git a/panel/graphql/queries.ts b/panel/graphql/queries.ts index b47d3244..6b45c6b0 100644 --- a/panel/graphql/queries.ts +++ b/panel/graphql/queries.ts @@ -34,18 +34,21 @@ export const ADMIN_GET_SHOUTS_QUERY: string = name email slug + created_at } updated_by { id name email slug + created_at } deleted_by { id name email slug + created_at } community { id @@ -57,6 +60,7 @@ export const ADMIN_GET_SHOUTS_QUERY: string = name email slug + created_at } topics { id @@ -210,6 +214,7 @@ export const ADMIN_GET_REACTIONS_QUERY: string = name email slug + created_at } shout { id diff --git a/panel/routes/reactions.tsx b/panel/routes/reactions.tsx index 2141d3d3..697626eb 100644 --- a/panel/routes/reactions.tsx +++ b/panel/routes/reactions.tsx @@ -1,4 +1,4 @@ -import { Component, createSignal, For, onMount, Show } from 'solid-js' +import { Component, createSignal, createEffect, For, onMount, Show } from 'solid-js' import { query } from '../graphql' import type { Query } from '../graphql/generated/schema' import { ADMIN_DELETE_REACTION_MUTATION, ADMIN_RESTORE_REACTION_MUTATION, ADMIN_UPDATE_REACTION_MUTATION } from '../graphql/mutations' @@ -31,6 +31,7 @@ interface AdminReaction { name: string email: string slug: string + created_at: number } shout: { id: number @@ -70,8 +71,7 @@ const ReactionsRoute: Component = (props) => { // Фильтры const [searchQuery, setSearchQuery] = createSignal('') const [kindFilter, setKindFilter] = createSignal('') - const [shoutIdFilter, setShoutIdFilter] = createSignal('') - const [statusFilter, setStatusFilter] = createSignal('all') + const [showDeletedOnly, setShowDeletedOnly] = createSignal(false) /** * Загрузка списка реакций @@ -80,6 +80,11 @@ const ReactionsRoute: Component = (props) => { console.log('[ReactionsRoute] Loading reactions...') try { setLoading(true) + + // Определяем, является ли поисковый запрос ID публикации + const query_value = searchQuery().trim() + const isShoutId = /^\d+$/.test(query_value) // Проверяем, состоит ли запрос только из цифр + const data = await query<{ adminGetReactions: { reactions: AdminReaction[] total: number @@ -90,10 +95,10 @@ const ReactionsRoute: Component = (props) => { `${location.origin}/graphql`, ADMIN_GET_REACTIONS_QUERY, { - search: searchQuery(), + search: isShoutId ? '' : query_value, // Если это ID, не передаем в обычный поиск kind: kindFilter() || undefined, - shout_id: shoutIdFilter() ? parseInt(shoutIdFilter()) : undefined, - status: statusFilter(), + shout_id: isShoutId ? parseInt(query_value) : undefined, // Если это ID, передаем в shout_id + status: showDeletedOnly() ? 'deleted' : 'all', limit: pagination().limit, offset: (pagination().page - 1) * pagination().limit } @@ -187,9 +192,28 @@ const ReactionsRoute: Component = (props) => { void loadReactions() } + // Флаг для пропуска первого вызова createEffect при монтировании + let isInitialized = false + // Load reactions on mount onMount(() => { console.log('[ReactionsRoute] Component mounted, loading reactions...') + isInitialized = true + void loadReactions() + }) + + // Автоматически применяем фильтры при изменении (но не при первом рендере) + createEffect(() => { + // Отслеживаем изменения фильтров и поиска + searchQuery() + kindFilter() + showDeletedOnly() + + // Пропускаем первый вызов при инициализации + if (!isInitialized) return + + // Сбрасываем страницу на первую и перезагружаем данные + setPagination((prev) => ({ ...prev, page: 1 })) void loadReactions() }) @@ -269,6 +293,35 @@ const ReactionsRoute: Component = (props) => { } } + /** + * Получает название статуса реакции + */ + const getReactionStatusTitle = (reaction: AdminReaction): string => { + return reaction.deleted_at ? 'Удалена' : 'Активна' + } + + /** + * Получает цвет фона для ID реакции в зависимости от статуса + */ + const getReactionStatusBackgroundColor = (reaction: AdminReaction): string => { + return reaction.deleted_at ? '#fee2e2' : '#d1fae5' // Пастельный красный для удаленных, зеленый для активных + } + + /** + * Форматирует tooltip для автора с email и датой регистрации + */ + const formatAuthorTooltip = (author: { email?: string | null; created_at?: number | null }): string => { + if (!author.email) return '' + if (!author.created_at) return author.email + + const registrationDate = new Date(author.created_at * 1000).toLocaleDateString('ru-RU', { + year: 'numeric', + month: '2-digit', + day: '2-digit' + }) + return `${author.email} с ${registrationDate}` + } + return (
@@ -281,7 +334,7 @@ const ReactionsRoute: Component = (props) => { searchValue={searchQuery()} onSearchChange={handleSearchChange} onSearch={handleSearch} - searchPlaceholder="Поиск по тексту, автору или публикации..." + searchPlaceholder="Поиск по тексту, автору, публикации или ID публикации..." isLoading={loading()} /> @@ -308,27 +361,14 @@ const ReactionsRoute: Component = (props) => { - - - setShoutIdFilter(e.target.value)} - class={styles['filter-input']} - /> - - +
@@ -347,7 +387,6 @@ const ReactionsRoute: Component = (props) => { Автор Публикация Создано - Статус Действия @@ -361,7 +400,16 @@ const ReactionsRoute: Component = (props) => { setShowEditModal(true) }} > - {reaction.id} + + {reaction.id} + {getReactionIcon(reaction.kind)} @@ -373,7 +421,7 @@ const ReactionsRoute: Component = (props) => { -
+
{reaction.created_by.name || 'Без имени'}
{reaction.created_by.email}
@@ -390,11 +438,6 @@ const ReactionsRoute: Component = (props) => {
{formatDateRelative(reaction.created_at)()} - - - {reaction.deleted_at ? 'Удалено' : 'Активно'} - -
e.stopPropagation()}> diff --git a/panel/routes/shouts.tsx b/panel/routes/shouts.tsx index 3e94859a..ab35952b 100644 --- a/panel/routes/shouts.tsx +++ b/panel/routes/shouts.tsx @@ -193,6 +193,21 @@ const ShoutsRoute = (props: ShoutsRouteProps) => { return `${text.substring(0, maxLength)}...` } + /** + * Форматирует tooltip для автора с email и датой регистрации + */ + function formatAuthorTooltip(author: { email?: string | null; created_at?: number | null }): string { + if (!author.email) return '' + if (!author.created_at) return author.email + + const registrationDate = new Date(author.created_at * 1000).toLocaleDateString('ru-RU', { + year: 'numeric', + month: '2-digit', + day: '2-digit' + }) + return `${author.email} с ${registrationDate}` + } + return (
@@ -258,7 +273,7 @@ const ShoutsRoute = (props: ShoutsRouteProps) => { {(author) => ( {(safeAuthor) => ( - + {safeAuthor()?.name || safeAuthor()?.email || `ID:${safeAuthor()?.id}`} )} diff --git a/panel/styles/Admin.module.css b/panel/styles/Admin.module.css index b6e96901..60cc3213 100644 --- a/panel/styles/Admin.module.css +++ b/panel/styles/Admin.module.css @@ -707,17 +707,17 @@ td { .reactions-list th:nth-child(3), /* ТЕКСТ */ .reactions-list td:nth-child(3) { - width: 25%; + width: 28%; } .reactions-list th:nth-child(4), /* АВТОР */ .reactions-list td:nth-child(4) { - width: 18%; + width: 20%; } .reactions-list th:nth-child(5), /* ПУБЛИКАЦИЯ */ .reactions-list td:nth-child(5) { - width: 22%; + width: 25%; } .reactions-list th:nth-child(6), /* СОЗДАНО */ @@ -725,14 +725,8 @@ td { width: 120px; } -.reactions-list th:nth-child(7), /* СТАТУС */ +.reactions-list th:nth-child(7), /* ДЕЙСТВИЯ */ .reactions-list td:nth-child(7) { - width: 100px; - text-align: center; -} - -.reactions-list th:nth-child(8), /* ДЕЙСТВИЯ */ -.reactions-list td:nth-child(8) { width: 120px; text-align: center; } @@ -847,6 +841,29 @@ td { box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2); } +.filter-checkbox { + display: flex; + align-items: center; + gap: 8px; + cursor: pointer; + padding: 6px 10px; + border-radius: 4px; + transition: all 0.2s ease; + user-select: none; + font-size: 0.875rem; +} + +.filter-checkbox:hover { + background-color: #f0f4f8; +} + +.filter-checkbox input[type="checkbox"] { + width: 16px; + height: 16px; + cursor: pointer; + accent-color: #3b82f6; +} + .stat-info { display: flex; gap: 1rem;