import { createEffect, createSignal, For, on, onMount, Show, untrack } from 'solid-js' import { useData } from '../context/data' import { useTableSort } from '../context/sort' import { SHOUTS_SORT_CONFIG } from '../context/sortConfig' import { query } from '../graphql' import type { Query, AdminShoutInfo as Shout } from '../graphql/generated/schema' import { ADMIN_GET_SHOUTS_QUERY } from '../graphql/queries' import styles from '../styles/Admin.module.css' import EditableCodePreview from '../ui/EditableCodePreview' import Modal from '../ui/Modal' import Pagination from '../ui/Pagination' import SortableHeader from '../ui/SortableHeader' import TableControls from '../ui/TableControls' import { formatDateRelative } from '../utils/date' export interface ShoutsRouteProps { onError?: (error: string) => void onSuccess?: (message: string) => void } const ShoutsRoute = (props: ShoutsRouteProps) => { const [shouts, setShouts] = createSignal([]) const [loading, setLoading] = createSignal(true) const [showBodyModal, setShowBodyModal] = createSignal(false) const [selectedShoutBody, setSelectedShoutBody] = createSignal('') const [showMediaBodyModal, setShowMediaBodyModal] = createSignal(false) const [selectedMediaBody, setSelectedMediaBody] = createSignal('') const { sortState } = useTableSort() const { selectedCommunity } = useData() // Pagination state const [pagination, setPagination] = createSignal<{ page: number limit: number total: number totalPages: number }>({ page: 1, limit: 20, total: 0, totalPages: 0 }) // Filter state const [searchQuery, setSearchQuery] = createSignal('') /** * Загрузка списка публикаций */ async function loadShouts() { try { setLoading(true) // Подготавливаем параметры запроса const variables: { limit: number offset: number search?: string community?: number } = { limit: pagination().limit, offset: (pagination().page - 1) * pagination().limit } // Добавляем поиск если есть if (searchQuery().trim()) { variables.search = searchQuery().trim() } // Добавляем фильтр по сообществу если выбрано const communityFilter = selectedCommunity() if (communityFilter !== null) { variables.community = communityFilter } const result = await query<{ adminGetShouts: Query['adminGetShouts'] }>( `${location.origin}/graphql`, ADMIN_GET_SHOUTS_QUERY, variables ) if (result?.adminGetShouts?.shouts) { // Применяем сортировку на клиенте const sortedShouts = sortShouts(result.adminGetShouts.shouts) setShouts(sortedShouts) setPagination((prev) => ({ ...prev, total: result.adminGetShouts.total || 0, totalPages: result.adminGetShouts.totalPages || 1 })) } } catch (error) { console.error('Failed to load shouts:', error) props.onError?.(error instanceof Error ? error.message : 'Failed to load shouts') } finally { setLoading(false) } } // Load shouts on mount onMount(() => { void loadShouts() }) // Pagination handlers function handlePageChange(page: number) { setPagination((prev) => ({ ...prev, page })) void loadShouts() } function handlePerPageChange(limit: number) { setPagination((prev) => ({ ...prev, page: 1, limit })) void loadShouts() } /** * Сортирует публикации на клиенте */ function sortShouts(shoutsData: Shout[]): Shout[] { const { field, direction } = sortState() return [...shoutsData].sort((a, b) => { let comparison = 0 switch (field) { case 'id': comparison = Number(a.id) - Number(b.id) break case 'title': comparison = (a.title || '').localeCompare(b.title || '', 'ru') break case 'slug': comparison = (a.slug || '').localeCompare(b.slug || '', 'ru') break case 'created_at': comparison = (a.created_at || 0) - (b.created_at || 0) break case 'published_at': comparison = (a.published_at || 0) - (b.published_at || 0) break case 'updated_at': comparison = (a.updated_at || 0) - (b.updated_at || 0) break default: comparison = Number(a.id) - Number(b.id) } return direction === 'desc' ? -comparison : comparison }) } // Пересортировка при изменении состояния сортировки createEffect( on([sortState], () => { if (shouts().length > 0) { // Используем untrack для предотвращения бесконечной рекурсии const currentShouts = untrack(() => shouts()) const sortedShouts = sortShouts(currentShouts) // Сравниваем текущий порядок с отсортированным, чтобы избежать лишних обновлений const needsUpdate = JSON.stringify(currentShouts.map((s: Shout) => s.id)) !== JSON.stringify(sortedShouts.map((s: Shout) => s.id)) if (needsUpdate) { setShouts(sortedShouts) } } }) ) // Перезагрузка при изменении выбранного сообщества createEffect( on([selectedCommunity], () => { void loadShouts() }) ) // Helper functions function getShoutStatusTitle(shout: Shout): string { if (shout.deleted_at) return 'Удалена' if (shout.published_at) return 'Опубликована' return 'Черновик' } function getShoutStatusBackgroundColor(shout: Shout): string { if (shout.deleted_at) return '#fee2e2' // Пастельный красный if (shout.published_at) return '#d1fae5' // Пастельный зеленый return '#fef3c7' // Пастельный желтый для черновиков } function truncateText(text: string, maxLength = 100): string { if (!text || text.length <= maxLength) return text return `${text.substring(0, maxLength)}...` } return (
Загрузка публикаций...
Нет публикаций для отображения
0}> setSearchQuery(value)} onSearch={() => void loadShouts()} />
ID Заголовок Slug Создан {(shout) => ( )}
Авторы Темы Содержимое Media
{shout.id} {truncateText(shout.title, 50)} {truncateText(shout.slug, 30)}
{(author) => ( {(safeAuthor) => ( {safeAuthor()?.name || safeAuthor()?.email || `ID:${safeAuthor()?.id}`} )} )}
-
{(topic) => ( {(safeTopic) => ( {safeTopic()?.title || safeTopic()?.slug} )} )}
-
{formatDateRelative(shout.created_at)()} { setSelectedShoutBody(shout.body) setShowBodyModal(true) }} style="cursor: pointer; max-width: 300px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;" > {truncateText(shout.body.replace(/<[^>]*>/g, ''), 100)} 0}>
{(mediaItem, idx) => (
)}
-
setShowBodyModal(false)} title="Содержимое публикации" size="large" > { setSelectedShoutBody(newContent) }} onSave={(_content) => { // FIXME: добавить логику сохранения изменений в базу данных props.onSuccess?.('Содержимое публикации обновлено') setShowBodyModal(false) }} onCancel={() => { setShowBodyModal(false) }} placeholder="Введите содержимое публикации..." /> setShowMediaBodyModal(false)} title="Содержимое media.body" size="large" > { setSelectedMediaBody(newContent) }} onSave={(_content) => { // FIXME: добавить логику сохранения изменений media.body props.onSuccess?.('Содержимое media.body обновлено') setShowMediaBodyModal(false) }} onCancel={() => { setShowMediaBodyModal(false) }} placeholder="Введите содержимое media.body..." />
) } export default ShoutsRoute