core/panel/routes/communities.tsx

416 lines
14 KiB
TypeScript
Raw Normal View History

2025-07-02 19:30:21 +00:00
import { Component, createEffect, createSignal, For, on, onMount, Show, untrack } from 'solid-js'
import { useTableSort } from '../context/sort'
import { COMMUNITIES_SORT_CONFIG } from '../context/sortConfig'
2025-06-30 19:19:46 +00:00
import {
CREATE_COMMUNITY_MUTATION,
DELETE_COMMUNITY_MUTATION,
UPDATE_COMMUNITY_MUTATION
} from '../graphql/mutations'
2025-06-30 18:25:26 +00:00
import { GET_COMMUNITIES_QUERY } from '../graphql/queries'
2025-06-30 19:19:46 +00:00
import CommunityEditModal from '../modals/CommunityEditModal'
2025-06-30 18:25:26 +00:00
import styles from '../styles/Table.module.css'
import Button from '../ui/Button'
import Modal from '../ui/Modal'
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
/**
* Интерфейс для сообщества (используем локальный интерфейс для совместимости)
*/
interface Community {
id: number
slug: string
name: string
desc?: string
pic: string
created_at: number
created_by: {
id: number
name: string
email: string
}
stat: {
shouts: number
followers: number
authors: number
}
}
interface CommunitiesRouteProps {
onError: (error: string) => void
onSuccess: (message: string) => void
}
/**
* Компонент для управления сообществами
*/
const CommunitiesRoute: Component<CommunitiesRouteProps> = (props) => {
const [communities, setCommunities] = createSignal<Community[]>([])
const [loading, setLoading] = createSignal(false)
2025-07-02 19:30:21 +00:00
const { sortState } = useTableSort()
const [editModal, setEditModal] = createSignal<{
show: boolean
community: Community | null
}>({
2025-06-30 18:25:26 +00:00
show: false,
community: null
})
2025-07-02 19:30:21 +00:00
const [deleteModal, setDeleteModal] = createSignal<{
show: boolean
community: Community | null
}>({
2025-06-30 18:25:26 +00:00
show: false,
community: null
})
2025-06-30 19:19:46 +00:00
const [createModal, setCreateModal] = createSignal<{ show: boolean }>({
show: false
2025-06-30 18:25:26 +00:00
})
/**
* Загружает список всех сообществ
*/
const loadCommunities = async () => {
setLoading(true)
try {
2025-07-02 19:30:21 +00:00
// Загружаем все сообщества без параметров сортировки
// Сортировка будет выполнена на клиенте
2025-06-30 18:25:26 +00:00
const response = await fetch('/graphql', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
query: GET_COMMUNITIES_QUERY
})
})
const result = await response.json()
if (result.errors) {
throw new Error(result.errors[0].message)
}
2025-07-02 19:30:21 +00:00
// Получаем данные и сортируем их на клиенте
const communitiesData = result.data.get_communities_all || []
const sortedCommunities = sortCommunities(communitiesData)
setCommunities(sortedCommunities)
2025-06-30 18:25:26 +00:00
} catch (error) {
props.onError(`Ошибка загрузки сообществ: ${(error as Error).message}`)
} finally {
setLoading(false)
}
}
/**
* Форматирует дату
*/
const formatDate = (timestamp: number): string => {
return new Date(timestamp * 1000).toLocaleDateString('ru-RU')
}
2025-07-02 19:30:21 +00:00
/**
* Сортирует сообщества на клиенте в соответствии с текущим состоянием сортировки
*/
const sortCommunities = (communities: Community[]): Community[] => {
const { field, direction } = sortState()
return [...communities].sort((a, b) => {
let comparison = 0
switch (field) {
case 'id':
comparison = a.id - b.id
break
case 'name':
comparison = (a.name || '').localeCompare(b.name || '', 'ru')
break
case 'slug':
comparison = (a.slug || '').localeCompare(b.slug || '', 'ru')
break
case 'created_at':
comparison = a.created_at - b.created_at
break
case 'created_by': {
const aName = a.created_by?.name || a.created_by?.email || ''
const bName = b.created_by?.name || b.created_by?.email || ''
comparison = aName.localeCompare(bName, 'ru')
break
}
case 'shouts':
comparison = (a.stat?.shouts || 0) - (b.stat?.shouts || 0)
break
case 'followers':
comparison = (a.stat?.followers || 0) - (b.stat?.followers || 0)
break
case 'authors':
comparison = (a.stat?.authors || 0) - (b.stat?.authors || 0)
break
default:
comparison = a.id - b.id
}
return direction === 'desc' ? -comparison : comparison
})
}
2025-06-30 19:19:46 +00:00
/**
* Открывает модалку создания
*/
const openCreateModal = () => {
setCreateModal({ show: true })
}
2025-06-30 18:25:26 +00:00
/**
* Открывает модалку редактирования
*/
const openEditModal = (community: Community) => {
setEditModal({ show: true, community })
}
/**
2025-06-30 19:19:46 +00:00
* Обрабатывает сохранение сообщества (создание или обновление)
2025-06-30 18:25:26 +00:00
*/
2025-06-30 19:19:46 +00:00
const handleSaveCommunity = async (communityData: Partial<Community>) => {
2025-06-30 18:25:26 +00:00
try {
2025-06-30 19:19:46 +00:00
const isCreating = !editModal().community && createModal().show
const mutation = isCreating ? CREATE_COMMUNITY_MUTATION : UPDATE_COMMUNITY_MUTATION
2025-06-30 18:25:26 +00:00
const response = await fetch('/graphql', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
2025-06-30 19:19:46 +00:00
query: mutation,
variables: { community_input: communityData }
2025-06-30 18:25:26 +00:00
})
})
const result = await response.json()
if (result.errors) {
throw new Error(result.errors[0].message)
}
2025-06-30 19:19:46 +00:00
const resultData = isCreating ? result.data.create_community : result.data.update_community
if (resultData.error) {
throw new Error(resultData.error)
2025-06-30 18:25:26 +00:00
}
2025-06-30 19:19:46 +00:00
props.onSuccess(isCreating ? 'Сообщество успешно создано' : 'Сообщество успешно обновлено')
setCreateModal({ show: false })
2025-06-30 18:25:26 +00:00
setEditModal({ show: false, community: null })
await loadCommunities()
} catch (error) {
2025-06-30 19:19:46 +00:00
props.onError(
`Ошибка ${createModal().show ? 'создания' : 'обновления'} сообщества: ${(error as Error).message}`
)
2025-06-30 18:25:26 +00:00
}
}
/**
* Удаляет сообщество
*/
const deleteCommunity = async (slug: string) => {
try {
const response = await fetch('/graphql', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
query: DELETE_COMMUNITY_MUTATION,
variables: { slug }
})
})
const result = await response.json()
if (result.errors) {
throw new Error(result.errors[0].message)
}
if (result.data.delete_community.error) {
throw new Error(result.data.delete_community.error)
}
props.onSuccess('Сообщество успешно удалено')
setDeleteModal({ show: false, community: null })
await loadCommunities()
} catch (error) {
props.onError(`Ошибка удаления сообщества: ${(error as Error).message}`)
}
}
2025-07-02 19:30:21 +00:00
// Пересортировка при изменении состояния сортировки
createEffect(
on([sortState], () => {
if (communities().length > 0) {
// Используем untrack для предотвращения бесконечной рекурсии
const currentCommunities = untrack(() => communities())
const sortedCommunities = sortCommunities(currentCommunities)
// Сравниваем текущий порядок с отсортированным, чтобы избежать лишних обновлений
const needsUpdate =
JSON.stringify(currentCommunities.map((c: Community) => c.id)) !==
JSON.stringify(sortedCommunities.map((c: Community) => c.id))
if (needsUpdate) {
setCommunities(sortedCommunities)
}
}
})
)
2025-06-30 18:25:26 +00:00
// Загружаем сообщества при монтировании компонента
onMount(() => {
void loadCommunities()
})
return (
<div class={styles.container}>
2025-07-02 19:30:21 +00:00
<TableControls
onRefresh={loadCommunities}
isLoading={loading()}
actions={
<Button variant="primary" onClick={openCreateModal}>
Создать сообщество
</Button>
}
/>
2025-06-30 18:25:26 +00:00
<Show
when={!loading()}
fallback={
<div class="loading-screen">
<div class="loading-spinner" />
<div>Загрузка сообществ...</div>
</div>
}
>
<table class={styles.table}>
<thead>
<tr>
2025-07-02 19:30:21 +00:00
<SortableHeader field="id" allowedFields={COMMUNITIES_SORT_CONFIG.allowedFields}>
ID
</SortableHeader>
<SortableHeader field="name" allowedFields={COMMUNITIES_SORT_CONFIG.allowedFields}>
Название
</SortableHeader>
<SortableHeader field="slug" allowedFields={COMMUNITIES_SORT_CONFIG.allowedFields}>
Slug
</SortableHeader>
2025-06-30 18:25:26 +00:00
<th>Описание</th>
2025-07-02 19:30:21 +00:00
<SortableHeader field="created_by" allowedFields={COMMUNITIES_SORT_CONFIG.allowedFields}>
Создатель
</SortableHeader>
<SortableHeader field="shouts" allowedFields={COMMUNITIES_SORT_CONFIG.allowedFields}>
Публикации
</SortableHeader>
<SortableHeader field="followers" allowedFields={COMMUNITIES_SORT_CONFIG.allowedFields}>
Подписчики
</SortableHeader>
2025-06-30 18:25:26 +00:00
<th>Авторы</th>
2025-07-02 19:30:21 +00:00
<SortableHeader field="created_at" allowedFields={COMMUNITIES_SORT_CONFIG.allowedFields}>
Создано
</SortableHeader>
2025-06-30 18:25:26 +00:00
<th>Действия</th>
</tr>
</thead>
<tbody>
<For each={communities()}>
{(community) => (
<tr
onClick={() => openEditModal(community)}
style={{ cursor: 'pointer' }}
class={styles['clickable-row']}
>
<td>{community.id}</td>
<td>{community.name}</td>
<td>{community.slug}</td>
<td>
<div
style={{
'max-width': '200px',
overflow: 'hidden',
'text-overflow': 'ellipsis',
'white-space': 'nowrap'
}}
title={community.desc}
>
{community.desc || '—'}
</div>
</td>
<td>{community.created_by.name || community.created_by.email}</td>
<td>{community.stat.shouts}</td>
<td>{community.stat.followers}</td>
<td>{community.stat.authors}</td>
<td>{formatDate(community.created_at)}</td>
<td onClick={(e) => e.stopPropagation()}>
<button
onClick={(e) => {
e.stopPropagation()
setDeleteModal({ show: true, community })
}}
class={styles['delete-button']}
title="Удалить сообщество"
aria-label="Удалить сообщество"
>
×
</button>
</td>
</tr>
)}
</For>
</tbody>
</table>
</Show>
2025-06-30 19:19:46 +00:00
{/* Модальное окно создания */}
<CommunityEditModal
isOpen={createModal().show}
community={null}
onClose={() => setCreateModal({ show: false })}
onSave={handleSaveCommunity}
/>
2025-06-30 18:25:26 +00:00
{/* Модальное окно редактирования */}
2025-06-30 19:19:46 +00:00
<CommunityEditModal
2025-06-30 18:25:26 +00:00
isOpen={editModal().show}
2025-06-30 19:19:46 +00:00
community={editModal().community}
2025-06-30 18:25:26 +00:00
onClose={() => setEditModal({ show: false, community: null })}
2025-06-30 19:19:46 +00:00
onSave={handleSaveCommunity}
/>
2025-06-30 18:25:26 +00:00
{/* Модальное окно подтверждения удаления */}
<Modal
isOpen={deleteModal().show}
onClose={() => setDeleteModal({ show: false, community: null })}
title="Подтверждение удаления"
>
<div>
<p>
Вы уверены, что хотите удалить сообщество "<strong>{deleteModal().community?.name}</strong>"?
</p>
<p class={styles['warning-text']}>
Это действие нельзя отменить. Все публикации и темы сообщества могут быть затронуты.
</p>
<div class={styles['modal-actions']}>
<Button variant="secondary" onClick={() => setDeleteModal({ show: false, community: null })}>
Отмена
</Button>
<Button
variant="danger"
onClick={() => deleteModal().community && deleteCommunity(deleteModal().community!.slug)}
>
Удалить
</Button>
</div>
</div>
</Modal>
</div>
)
}
export default CommunitiesRoute