2025-07-02 19:30:21 +00:00
|
|
|
|
import { createEffect, createSignal, For, on, Show } from 'solid-js'
|
|
|
|
|
import { Topic, useData } from '../context/data'
|
|
|
|
|
import { useTableSort } from '../context/sort'
|
|
|
|
|
import { TOPICS_SORT_CONFIG } from '../context/sortConfig'
|
2025-06-30 18:25:26 +00:00
|
|
|
|
import TopicEditModal from '../modals/TopicEditModal'
|
2025-07-02 19:30:21 +00:00
|
|
|
|
import adminStyles from '../styles/Admin.module.css'
|
2025-06-30 18:25:26 +00:00
|
|
|
|
import styles from '../styles/Table.module.css'
|
2025-07-02 19:30:21 +00:00
|
|
|
|
import SortableHeader from '../ui/SortableHeader'
|
|
|
|
|
import TableControls from '../ui/TableControls'
|
2025-06-30 18:25:26 +00:00
|
|
|
|
|
2025-07-02 19:30:21 +00:00
|
|
|
|
interface TopicsProps {
|
|
|
|
|
onError?: (message: string) => void
|
|
|
|
|
onSuccess?: (message: string) => void
|
2025-06-30 18:25:26 +00:00
|
|
|
|
}
|
|
|
|
|
|
2025-07-02 19:30:21 +00:00
|
|
|
|
export const Topics = (props: TopicsProps) => {
|
2025-07-03 09:15:10 +00:00
|
|
|
|
const { selectedCommunity, loadTopicsByCommunity, topics: contextTopics, getTopicTitle } = useData()
|
2025-06-30 18:25:26 +00:00
|
|
|
|
|
2025-07-02 19:30:21 +00:00
|
|
|
|
// Состояние поиска
|
|
|
|
|
const [searchQuery, setSearchQuery] = createSignal('')
|
|
|
|
|
|
|
|
|
|
// Состояние загрузки
|
2025-06-30 18:25:26 +00:00
|
|
|
|
const [loading, setLoading] = createSignal(false)
|
2025-07-02 19:30:21 +00:00
|
|
|
|
|
|
|
|
|
// Модальное окно для редактирования топика
|
|
|
|
|
const [showEditModal, setShowEditModal] = createSignal(false)
|
|
|
|
|
const [selectedTopic, setSelectedTopic] = createSignal<Topic | undefined>(undefined)
|
|
|
|
|
|
|
|
|
|
// Сортировка
|
|
|
|
|
const { sortState } = useTableSort()
|
2025-06-30 18:25:26 +00:00
|
|
|
|
|
|
|
|
|
/**
|
2025-07-02 19:30:21 +00:00
|
|
|
|
* Загрузка топиков для сообщества
|
2025-06-30 18:25:26 +00:00
|
|
|
|
*/
|
2025-07-02 19:30:21 +00:00
|
|
|
|
async function loadTopicsForCommunity() {
|
|
|
|
|
const community = selectedCommunity()
|
|
|
|
|
// selectedCommunity теперь всегда число (по умолчанию 1)
|
|
|
|
|
|
|
|
|
|
console.log('[TopicsRoute] Loading all topics for community...')
|
2025-06-30 18:25:26 +00:00
|
|
|
|
try {
|
2025-07-02 19:30:21 +00:00
|
|
|
|
setLoading(true)
|
2025-06-30 18:25:26 +00:00
|
|
|
|
|
2025-07-02 19:30:21 +00:00
|
|
|
|
// Загружаем все топики сообщества
|
|
|
|
|
await loadTopicsByCommunity(community!)
|
|
|
|
|
|
|
|
|
|
console.log('[TopicsRoute] All topics loaded')
|
2025-06-30 18:25:26 +00:00
|
|
|
|
} catch (error) {
|
2025-07-02 19:30:21 +00:00
|
|
|
|
console.error('[TopicsRoute] Failed to load topics:', error)
|
|
|
|
|
props.onError?.(error instanceof Error ? error.message : 'Не удалось загрузить список топиков')
|
2025-06-30 18:25:26 +00:00
|
|
|
|
} finally {
|
|
|
|
|
setLoading(false)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2025-07-02 19:30:21 +00:00
|
|
|
|
* Обработчик поиска - применяет поисковый запрос
|
2025-06-30 18:25:26 +00:00
|
|
|
|
*/
|
2025-07-02 19:30:21 +00:00
|
|
|
|
const handleSearch = () => {
|
|
|
|
|
// Поиск осуществляется через filteredTopics(), которая реагирует на searchQuery()
|
|
|
|
|
// Дополнительная логика поиска здесь не нужна, но можно добавить аналитику
|
|
|
|
|
console.log('[TopicsRoute] Search triggered with query:', searchQuery())
|
|
|
|
|
}
|
2025-06-30 18:25:26 +00:00
|
|
|
|
|
2025-07-02 19:30:21 +00:00
|
|
|
|
/**
|
|
|
|
|
* Фильтрация топиков по поисковому запросу
|
|
|
|
|
*/
|
|
|
|
|
const filteredTopics = () => {
|
|
|
|
|
const topics = contextTopics()
|
|
|
|
|
const query = searchQuery().toLowerCase()
|
|
|
|
|
|
|
|
|
|
if (!query) return topics
|
|
|
|
|
|
|
|
|
|
return topics.filter(
|
|
|
|
|
(topic) =>
|
|
|
|
|
topic.title?.toLowerCase().includes(query) ||
|
|
|
|
|
topic.slug?.toLowerCase().includes(query) ||
|
|
|
|
|
topic.id.toString().includes(query)
|
|
|
|
|
)
|
2025-06-30 18:25:26 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2025-07-02 19:30:21 +00:00
|
|
|
|
* Сортировка топиков на клиенте
|
2025-06-30 18:25:26 +00:00
|
|
|
|
*/
|
2025-07-02 19:30:21 +00:00
|
|
|
|
const sortedTopics = () => {
|
|
|
|
|
const topics = filteredTopics()
|
|
|
|
|
const { field, direction } = sortState()
|
2025-06-30 18:25:26 +00:00
|
|
|
|
|
2025-07-02 19:30:21 +00:00
|
|
|
|
return [...topics].sort((a, b) => {
|
2025-06-30 18:25:26 +00:00
|
|
|
|
let comparison = 0
|
|
|
|
|
|
2025-07-02 19:30:21 +00:00
|
|
|
|
switch (field) {
|
|
|
|
|
case 'id':
|
|
|
|
|
comparison = a.id - b.id
|
|
|
|
|
break
|
|
|
|
|
case 'title':
|
|
|
|
|
comparison = (a.title || '').localeCompare(b.title || '', 'ru')
|
|
|
|
|
break
|
|
|
|
|
case 'slug':
|
|
|
|
|
comparison = (a.slug || '').localeCompare(b.slug || '', 'ru')
|
|
|
|
|
break
|
|
|
|
|
default:
|
|
|
|
|
comparison = a.id - b.id
|
2025-06-30 18:25:26 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return direction === 'desc' ? -comparison : comparison
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-02 19:30:21 +00:00
|
|
|
|
// Загрузка при смене сообщества
|
|
|
|
|
createEffect(
|
|
|
|
|
on(selectedCommunity, (updatedCommunity) => {
|
|
|
|
|
if (updatedCommunity) {
|
|
|
|
|
// selectedCommunity теперь всегда число, поэтому всегда загружаем
|
|
|
|
|
void loadTopicsForCommunity()
|
2025-06-30 18:25:26 +00:00
|
|
|
|
}
|
|
|
|
|
})
|
2025-07-02 19:30:21 +00:00
|
|
|
|
)
|
2025-06-30 18:25:26 +00:00
|
|
|
|
|
2025-07-02 19:30:21 +00:00
|
|
|
|
const truncateText = (text: string, maxLength = 100): string => {
|
|
|
|
|
if (!text || text.length <= maxLength) return text
|
|
|
|
|
return `${text.substring(0, maxLength)}...`
|
2025-06-30 22:20:48 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2025-07-02 19:30:21 +00:00
|
|
|
|
* Открытие модального окна редактирования топика
|
2025-06-30 22:20:48 +00:00
|
|
|
|
*/
|
2025-07-02 19:30:21 +00:00
|
|
|
|
const handleTopicEdit = (topic: Topic) => {
|
|
|
|
|
console.log('[TopicsRoute] Opening edit modal for topic:', topic)
|
|
|
|
|
setSelectedTopic(topic)
|
|
|
|
|
setShowEditModal(true)
|
2025-06-30 22:20:48 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2025-07-02 19:30:21 +00:00
|
|
|
|
* Сохранение изменений топика
|
2025-06-30 22:20:48 +00:00
|
|
|
|
*/
|
2025-07-03 09:15:10 +00:00
|
|
|
|
const handleTopicSave = async (updatedTopic: Topic) => {
|
2025-07-02 19:30:21 +00:00
|
|
|
|
console.log('[TopicsRoute] Saving topic:', updatedTopic)
|
2025-07-03 09:15:10 +00:00
|
|
|
|
console.log('[TopicsRoute] Topic parent_ids:', updatedTopic.parent_ids)
|
2025-06-30 22:20:48 +00:00
|
|
|
|
|
2025-07-03 09:15:10 +00:00
|
|
|
|
// Сразу обновляем локальные данные для мгновенного отображения
|
|
|
|
|
const currentTopics = contextTopics()
|
|
|
|
|
console.log('[TopicsRoute] Current topics count:', currentTopics.length)
|
|
|
|
|
|
|
|
|
|
const updatedTopics = currentTopics.map(topic =>
|
|
|
|
|
topic.id === updatedTopic.id ? updatedTopic : topic
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
console.log('[TopicsRoute] Updated topics count:', updatedTopics.length)
|
|
|
|
|
|
|
|
|
|
// Обновляем состояние контекста напрямую (это сработает мгновенно)
|
|
|
|
|
const { setTopics } = useData()
|
|
|
|
|
setTopics(updatedTopics)
|
2025-06-30 22:20:48 +00:00
|
|
|
|
|
2025-07-02 19:30:21 +00:00
|
|
|
|
props.onSuccess?.('Топик успешно обновлён')
|
2025-06-30 22:20:48 +00:00
|
|
|
|
|
2025-07-03 09:15:10 +00:00
|
|
|
|
// Ждем большее время чтобы сервер точно обработал изменения и инвалидировал кеш
|
|
|
|
|
console.log('[TopicsRoute] Scheduling reload in 500ms...')
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
console.log('[TopicsRoute] Reloading topics from server...')
|
|
|
|
|
void loadTopicsForCommunity()
|
|
|
|
|
}, 500)
|
2025-06-30 22:20:48 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2025-07-02 19:30:21 +00:00
|
|
|
|
* Обработка ошибок из модального окна
|
2025-06-30 22:20:48 +00:00
|
|
|
|
*/
|
2025-07-02 19:30:21 +00:00
|
|
|
|
const handleTopicError = (message: string) => {
|
|
|
|
|
props.onError?.(message)
|
2025-06-30 22:20:48 +00:00
|
|
|
|
}
|
|
|
|
|
|
2025-07-03 09:15:10 +00:00
|
|
|
|
/**
|
|
|
|
|
* Рендер родительских тем для топика
|
|
|
|
|
*/
|
|
|
|
|
const renderParentTopics = (parentIds?: number[]) => {
|
|
|
|
|
if (!parentIds || parentIds.length === 0) {
|
|
|
|
|
return <span style="color: #999; font-style: italic;">Нет родителей</span>
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div style="display: flex; flex-wrap: wrap; gap: 4px;">
|
|
|
|
|
<For each={parentIds}>
|
|
|
|
|
{(parentId) => {
|
|
|
|
|
const parentTitle = getTopicTitle(parentId)
|
|
|
|
|
return (
|
|
|
|
|
<span
|
|
|
|
|
style="
|
|
|
|
|
background: #e3f2fd;
|
|
|
|
|
color: #1976d2;
|
|
|
|
|
padding: 2px 6px;
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
font-size: 0.75rem;
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
"
|
|
|
|
|
title={`ID: ${parentId}`}
|
|
|
|
|
>
|
|
|
|
|
#{parentTitle || `ID:${parentId}`}
|
|
|
|
|
</span>
|
|
|
|
|
)
|
|
|
|
|
}}
|
|
|
|
|
</For>
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-30 18:25:26 +00:00
|
|
|
|
/**
|
2025-07-02 19:30:21 +00:00
|
|
|
|
* Рендер строки топика
|
2025-06-30 18:25:26 +00:00
|
|
|
|
*/
|
2025-07-02 19:30:21 +00:00
|
|
|
|
const renderTopicRow = (topic: Topic) => (
|
|
|
|
|
<tr
|
|
|
|
|
class={styles.tableRow}
|
|
|
|
|
onClick={() => handleTopicEdit(topic)}
|
|
|
|
|
style="cursor: pointer;"
|
|
|
|
|
title="Нажмите для редактирования топика"
|
|
|
|
|
>
|
|
|
|
|
<td class={styles.tableCell}>{topic.id}</td>
|
|
|
|
|
<td class={styles.tableCell}>
|
|
|
|
|
<strong title={topic.title}>{truncateText(topic.title, 50)}</strong>
|
|
|
|
|
</td>
|
|
|
|
|
<td class={styles.tableCell} title={topic.slug}>
|
|
|
|
|
{truncateText(topic.slug, 30)}
|
|
|
|
|
</td>
|
2025-07-03 09:15:10 +00:00
|
|
|
|
<td class={styles.tableCell}>
|
|
|
|
|
{renderParentTopics(topic.parent_ids)}
|
|
|
|
|
</td>
|
2025-07-02 19:30:21 +00:00
|
|
|
|
<td class={styles.tableCell}>
|
|
|
|
|
{topic.body ? (
|
|
|
|
|
<span style="color: #666;">{truncateText(topic.body.replace(/<[^>]*>/g, ''), 60)}</span>
|
|
|
|
|
) : (
|
|
|
|
|
<span style="color: #999; font-style: italic;">Нет содержимого</span>
|
|
|
|
|
)}
|
|
|
|
|
</td>
|
|
|
|
|
</tr>
|
|
|
|
|
)
|
2025-06-30 18:25:26 +00:00
|
|
|
|
|
|
|
|
|
return (
|
2025-07-02 19:30:21 +00:00
|
|
|
|
<div class={adminStyles.pageContainer}>
|
|
|
|
|
<TableControls
|
|
|
|
|
searchValue={searchQuery()}
|
|
|
|
|
onSearchChange={setSearchQuery}
|
|
|
|
|
onSearch={handleSearch}
|
|
|
|
|
searchPlaceholder="Поиск по названию, slug или ID..."
|
|
|
|
|
isLoading={loading()}
|
|
|
|
|
onRefresh={loadTopicsForCommunity}
|
|
|
|
|
/>
|
2025-06-30 18:25:26 +00:00
|
|
|
|
|
2025-07-02 19:30:21 +00:00
|
|
|
|
<div class={styles.tableContainer}>
|
2025-06-30 18:25:26 +00:00
|
|
|
|
<table class={styles.table}>
|
|
|
|
|
<thead>
|
2025-07-02 19:30:21 +00:00
|
|
|
|
<tr class={styles.tableHeader}>
|
|
|
|
|
<SortableHeader field="id" allowedFields={TOPICS_SORT_CONFIG.allowedFields}>
|
|
|
|
|
ID
|
|
|
|
|
</SortableHeader>
|
|
|
|
|
<SortableHeader field="title" allowedFields={TOPICS_SORT_CONFIG.allowedFields}>
|
|
|
|
|
Название
|
|
|
|
|
</SortableHeader>
|
|
|
|
|
<SortableHeader field="slug" allowedFields={TOPICS_SORT_CONFIG.allowedFields}>
|
|
|
|
|
Slug
|
|
|
|
|
</SortableHeader>
|
2025-07-03 09:15:10 +00:00
|
|
|
|
<th class={styles.tableHeaderCell}>Родительские темы</th>
|
2025-07-02 19:30:21 +00:00
|
|
|
|
<th class={styles.tableHeaderCell}>Body</th>
|
2025-06-30 18:25:26 +00:00
|
|
|
|
</tr>
|
|
|
|
|
</thead>
|
|
|
|
|
<tbody>
|
2025-07-02 19:30:21 +00:00
|
|
|
|
<Show when={loading()}>
|
|
|
|
|
<tr>
|
2025-07-03 09:15:10 +00:00
|
|
|
|
<td colspan="5" class={styles.loadingCell}>
|
2025-07-02 19:30:21 +00:00
|
|
|
|
Загрузка...
|
|
|
|
|
</td>
|
|
|
|
|
</tr>
|
|
|
|
|
</Show>
|
|
|
|
|
<Show when={!loading() && sortedTopics().length === 0}>
|
|
|
|
|
<tr>
|
2025-07-03 09:15:10 +00:00
|
|
|
|
<td colspan="5" class={styles.emptyCell}>
|
2025-07-02 19:30:21 +00:00
|
|
|
|
Нет топиков
|
|
|
|
|
</td>
|
|
|
|
|
</tr>
|
|
|
|
|
</Show>
|
|
|
|
|
<Show when={!loading()}>
|
|
|
|
|
<For each={sortedTopics()}>{renderTopicRow}</For>
|
|
|
|
|
</Show>
|
2025-06-30 18:25:26 +00:00
|
|
|
|
</tbody>
|
|
|
|
|
</table>
|
2025-07-02 19:30:21 +00:00
|
|
|
|
</div>
|
2025-06-30 18:25:26 +00:00
|
|
|
|
|
2025-07-02 19:30:21 +00:00
|
|
|
|
{/* Модальное окно для редактирования топика */}
|
2025-06-30 18:25:26 +00:00
|
|
|
|
<TopicEditModal
|
2025-07-02 19:30:21 +00:00
|
|
|
|
isOpen={showEditModal()}
|
|
|
|
|
topic={selectedTopic()!}
|
2025-06-30 22:20:48 +00:00
|
|
|
|
onClose={() => {
|
2025-07-02 19:30:21 +00:00
|
|
|
|
setShowEditModal(false)
|
|
|
|
|
setSelectedTopic(undefined)
|
2025-06-30 22:20:48 +00:00
|
|
|
|
}}
|
2025-07-02 19:30:21 +00:00
|
|
|
|
onSave={handleTopicSave}
|
|
|
|
|
onError={handleTopicError}
|
2025-06-30 22:20:48 +00:00
|
|
|
|
/>
|
2025-06-30 18:25:26 +00:00
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|