import { Component, createSignal, For, onMount, Show } from 'solid-js' 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 { formatDateRelative } from '../utils/date' export interface ShoutsRouteProps { onError?: (error: string) => void onSuccess?: (message: string) => void } const ShoutsRoute: Component = (props) => { 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('') // 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 result = await query<{ adminGetShouts: Query['adminGetShouts'] }>( `${location.origin}/graphql`, ADMIN_GET_SHOUTS_QUERY, { limit: pagination().limit, offset: (pagination().page - 1) * pagination().limit } ) if (result?.adminGetShouts?.shouts) { setShouts(result.adminGetShouts.shouts) 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() } // Helper functions function getShoutStatus(shout: Shout): string { if (shout.deleted_at) return '🗑️' if (shout.published_at) return '✅' return '📝' } function getShoutStatusTitle(shout: Shout): string { if (shout.deleted_at) return 'Удалена' if (shout.published_at) return 'Опубликована' return 'Черновик' } function getShoutStatusClass(shout: Shout): string { if (shout.deleted_at) return 'status-deleted' if (shout.published_at) return 'status-published' return 'status-draft' } function truncateText(text: string, maxLength = 100): string { if (!text || text.length <= maxLength) return text return `${text.substring(0, maxLength)}...` } return (
Загрузка публикаций...
Нет публикаций для отображения
0}>
setSearchQuery(e.currentTarget.value)} onKeyDown={(e) => { if (e.key === 'Enter') { void loadShouts() } }} class={styles['search-input']} />
{(shout) => ( )}
ID Заголовок Slug Статус Авторы Темы Создан Содержимое Media
{shout.id} {truncateText(shout.title, 50)} {truncateText(shout.slug, 30)} {getShoutStatus(shout)}
{(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) => (
{mediaItem?.title || `media[${idx()}]`}
)}
-
setShowBodyModal(false)} title="Содержимое публикации"> { setSelectedShoutBody(newContent) }} onSave={(_content) => { // FIXME: добавить логику сохранения изменений в базу данных props.onSuccess?.('Содержимое публикации обновлено') setShowBodyModal(false) }} onCancel={() => { setShowBodyModal(false) }} placeholder="Введите содержимое публикации..." /> setShowMediaBodyModal(false)} title="Содержимое media.body" > { setSelectedMediaBody(newContent) }} onSave={(_content) => { // FIXME: добавить логику сохранения изменений media.body props.onSuccess?.('Содержимое media.body обновлено') setShowMediaBodyModal(false) }} onCancel={() => { setShowMediaBodyModal(false) }} placeholder="Введите содержимое media.body..." />
) } export default ShoutsRoute