import { Component, createEffect, createSignal, For, onMount, Show } from 'solid-js' import { query } from '../graphql' import { ADMIN_DELETE_REACTION_MUTATION, ADMIN_RESTORE_REACTION_MUTATION, ADMIN_UPDATE_REACTION_MUTATION } from '../graphql/mutations' import { ADMIN_GET_REACTIONS_QUERY } from '../graphql/queries' import ReactionEditModal from '../modals/ReactionEditModal' import styles from '../styles/Admin.module.css' import Button from '../ui/Button' import Pagination from '../ui/Pagination' import TableControls from '../ui/TableControls' import { formatDateRelative } from '../utils/date' export interface ReactionsRouteProps { onError?: (error: string) => void onSuccess?: (message: string) => void } /** * Тип реакции для админки */ interface AdminReaction { id: number kind: string body: string created_at: number updated_at?: number deleted_at?: number reply_to?: number created_by: { id: number name: string email: string slug: string created_at: number } shout: { id: number title: string slug: string layout: string created_at: number published_at?: number deleted_at?: number } stat: { comments_count: number rating: number } } const ReactionsRoute: Component = (props) => { console.log('[ReactionsRoute] Initializing...') const [reactions, setReactions] = createSignal([]) const [loading, setLoading] = createSignal(true) const [selectedReaction, setSelectedReaction] = createSignal(null) const [showEditModal, setShowEditModal] = createSignal(false) // Pagination state const [pagination, setPagination] = createSignal<{ page: number limit: number total: number totalPages: number }>({ page: 1, limit: 20, total: 0, totalPages: 1 }) // Фильтры const [searchQuery, setSearchQuery] = createSignal('') const [kindFilter, setKindFilter] = createSignal('') const [showDeletedOnly, setShowDeletedOnly] = createSignal(false) /** * Загрузка списка реакций */ async function loadReactions() { 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 page: number perPage: number totalPages: number } }>(`${location.origin}/graphql`, ADMIN_GET_REACTIONS_QUERY, { search: isShoutId ? '' : query_value, // Если это ID, не передаем в обычный поиск kind: kindFilter() || undefined, shout_id: isShoutId ? Number.parseInt(query_value) : undefined, // Если это ID, передаем в shout_id status: showDeletedOnly() ? 'deleted' : 'all', limit: pagination().limit, offset: (pagination().page - 1) * pagination().limit }) if (data?.adminGetReactions?.reactions) { console.log('[ReactionsRoute] Reactions loaded:', data.adminGetReactions.reactions.length) setReactions(data.adminGetReactions.reactions as AdminReaction[]) setPagination((prev) => ({ ...prev, total: data.adminGetReactions.total || 0, totalPages: data.adminGetReactions.totalPages || 1 })) } } catch (error) { console.error('[ReactionsRoute] Failed to load reactions:', error) props.onError?.(error instanceof Error ? error.message : 'Не удалось загрузить список реакций') } finally { setLoading(false) } } /** * Обновляет реакцию */ async function updateReaction(reactionData: { id: number; body?: string; deleted_at?: number }) { try { await query(`${location.origin}/graphql`, ADMIN_UPDATE_REACTION_MUTATION, { reaction: reactionData }) closeEditModal() props.onSuccess?.('Реакция успешно обновлена') void loadReactions() } catch (err) { console.error('Ошибка обновления реакции:', err) props.onError?.(err instanceof Error ? err.message : 'Ошибка обновления реакции') } } /** * Удаляет реакцию */ async function deleteReaction(id: number) { try { await query(`${location.origin}/graphql`, ADMIN_DELETE_REACTION_MUTATION, { reaction_id: id }) props.onSuccess?.('Реакция успешно удалена') void loadReactions() } catch (err) { console.error('Ошибка удаления реакции:', err) props.onError?.(err instanceof Error ? err.message : 'Ошибка удаления реакции') } } /** * Восстанавливает реакцию */ async function restoreReaction(id: number) { try { await query(`${location.origin}/graphql`, ADMIN_RESTORE_REACTION_MUTATION, { reaction_id: id }) props.onSuccess?.('Реакция успешно восстановлена') void loadReactions() } catch (err) { console.error('Ошибка восстановления реакции:', err) props.onError?.(err instanceof Error ? err.message : 'Ошибка восстановления реакции') } } function closeEditModal() { setShowEditModal(false) setSelectedReaction(null) } // Pagination handlers function handlePageChange(page: number) { setPagination((prev) => ({ ...prev, page })) void loadReactions() } function handlePerPageChange(limit: number) { setPagination((prev) => ({ ...prev, page: 1, limit })) void loadReactions() } // Search handlers function handleSearchChange(value: string) { setSearchQuery(value) } function handleSearch() { setPagination((prev) => ({ ...prev, page: 1 })) 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() }) /** * Получает эмоджи для типа реакции */ const getReactionIcon = (kind: string): string => { switch (kind) { case 'LIKE': return '👍' case 'DISLIKE': return '👎' case 'COMMENT': return '💬' case 'QUOTE': return '❝' case 'AGREE': return '✅' case 'DISAGREE': return '❌' case 'ASK': return '❓' case 'PROPOSE': return '💡' case 'PROOF': return '🔬' case 'DISPROOF': return '🚫' case 'ACCEPT': return '✔️' case 'REJECT': return '❌' case 'CREDIT': return '🎨' case 'SILENT': return '🤫' default: return '💬' } } /** * Получает название типа реакции на русском */ const getReactionName = (kind: string): string => { switch (kind) { case 'LIKE': return 'Лайк' case 'DISLIKE': return 'Дизлайк' case 'COMMENT': return 'Комментарий' case 'QUOTE': return 'Цитата' case 'AGREE': return 'Согласен' case 'DISAGREE': return 'Не согласен' case 'ASK': return 'Вопрос' case 'PROPOSE': return 'Предложение' case 'PROOF': return 'Доказательство' case 'DISPROOF': return 'Опровержение' case 'ACCEPT': return 'Принять' case 'REJECT': return 'Отклонить' case 'CREDIT': return 'Упоминание' case 'SILENT': return 'Причастность' default: return kind } } /** * Получает название статуса реакции */ 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 (
Загрузка данных...
Нет данных для отображения
0}>
{(reaction) => ( { setSelectedReaction(reaction) setShowEditModal(true) }} > )}
ID Тип Текст Автор Публикация Создано Действия
{reaction.id} {getReactionIcon(reaction.kind)}
{reaction.body ? reaction.body.substring(0, 100) + (reaction.body.length > 100 ? '...' : '') : '-'}
{reaction.created_by.name || 'Без имени'}
{reaction.created_by.email}
{reaction.shout.title.substring(0, 50)} {reaction.shout.title.length > 50 ? '...' : ''}
ID: {reaction.shout.id} | {reaction.shout.slug}
{formatDateRelative(reaction.created_at)()}
e.stopPropagation()}>
) } export default ReactionsRoute