core/panel/routes/topics.tsx

251 lines
8.1 KiB
TypeScript
Raw Normal View History

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) => {
const { selectedCommunity, loadTopicsByCommunity, topics: contextTopics } = 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-02 19:30:21 +00:00
const handleTopicSave = (updatedTopic: Topic) => {
console.log('[TopicsRoute] Saving topic:', updatedTopic)
2025-06-30 22:20:48 +00:00
2025-07-02 19:30:21 +00:00
// TODO: добавить логику сохранения изменений в базу данных
// await updateTopic(updatedTopic)
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-02 19:30:21 +00:00
// Обновляем локальные данные (пока что просто перезагружаем)
void loadTopicsForCommunity()
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-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>
<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>
<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>
<td colspan="4" class={styles.loadingCell}>
Загрузка...
</td>
</tr>
</Show>
<Show when={!loading() && sortedTopics().length === 0}>
<tr>
<td colspan="4" class={styles.emptyCell}>
Нет топиков
</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
<div class={styles.tableFooter}>
<span class={styles.resultsInfo}>
<span>Всего</span>: {sortedTopics().length}
</span>
</div>
2025-06-30 19:19:46 +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>
)
}