core/panel/routes/collections.tsx

365 lines
11 KiB
TypeScript
Raw Normal View History

2025-06-30 18:46:53 +00:00
import { Component, createSignal, For, onMount, Show } from 'solid-js'
import {
CREATE_COLLECTION_MUTATION,
DELETE_COLLECTION_MUTATION,
UPDATE_COLLECTION_MUTATION
} from '../graphql/mutations'
import { GET_COLLECTIONS_QUERY } from '../graphql/queries'
2025-06-30 19:19:46 +00:00
import CollectionEditModal from '../modals/CollectionEditModal'
2025-06-30 18:46:53 +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 TableControls from '../ui/TableControls'
2025-06-30 18:46:53 +00:00
/**
* Интерфейс для коллекции
*/
interface Collection {
id: number
slug: string
title: string
desc?: string
pic: string
amount: number
created_at: number
published_at?: number
created_by: {
id: number
name: string
email: string
}
}
interface CollectionsRouteProps {
onError: (error: string) => void
onSuccess: (message: string) => void
}
/**
* Компонент для управления коллекциями
*/
const CollectionsRoute: Component<CollectionsRouteProps> = (props) => {
const [collections, setCollections] = createSignal<Collection[]>([])
2025-07-02 19:30:21 +00:00
const [filteredCollections, setFilteredCollections] = createSignal<Collection[]>([])
2025-06-30 18:46:53 +00:00
const [loading, setLoading] = createSignal(false)
2025-07-02 19:30:21 +00:00
const [searchQuery, setSearchQuery] = createSignal('')
const [editModal, setEditModal] = createSignal<{
show: boolean
collection: Collection | null
}>({
2025-06-30 18:46:53 +00:00
show: false,
collection: null
})
2025-07-02 19:30:21 +00:00
const [deleteModal, setDeleteModal] = createSignal<{
show: boolean
collection: Collection | null
}>({
2025-06-30 18:46:53 +00:00
show: false,
collection: null
})
const [createModal, setCreateModal] = createSignal(false)
/**
* Загружает список всех коллекций
*/
const loadCollections = async () => {
setLoading(true)
try {
const response = await fetch('/graphql', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
query: GET_COLLECTIONS_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 allCollections = result.data.get_collections_all || []
setCollections(allCollections)
filterCollections(allCollections, searchQuery())
2025-06-30 18:46:53 +00:00
} catch (error) {
props.onError(`Ошибка загрузки коллекций: ${(error as Error).message}`)
} finally {
setLoading(false)
}
}
2025-07-02 19:30:21 +00:00
/**
* Фильтрует коллекции по поисковому запросу
*/
const filterCollections = (allCollections: Collection[], query: string) => {
if (!query) {
setFilteredCollections(allCollections)
return
}
const lowerQuery = query.toLowerCase()
const filtered = allCollections.filter(
(collection) =>
collection.title.toLowerCase().includes(lowerQuery) ||
collection.slug.toLowerCase().includes(lowerQuery) ||
collection.id.toString().includes(lowerQuery) ||
collection.desc?.toLowerCase().includes(lowerQuery)
)
setFilteredCollections(filtered)
}
/**
* Обрабатывает изменение поискового запроса
*/
const handleSearchChange = (value: string) => {
setSearchQuery(value)
filterCollections(collections(), value)
}
/**
* Обработчик поиска - применяет текущий поисковый запрос
*/
const handleSearch = () => {
filterCollections(collections(), searchQuery())
console.log('[CollectionsRoute] Search triggered with query:', searchQuery())
}
2025-06-30 18:46:53 +00:00
/**
* Форматирует дату
*/
const formatDate = (timestamp: number): string => {
return new Date(timestamp * 1000).toLocaleDateString('ru-RU')
}
/**
* Открывает модалку редактирования
*/
const openEditModal = (collection: Collection) => {
setEditModal({ show: true, collection })
}
/**
* Открывает модалку создания
*/
const openCreateModal = () => {
setCreateModal(true)
}
/**
2025-06-30 19:19:46 +00:00
* Обрабатывает сохранение коллекции (создание или обновление)
2025-06-30 18:46:53 +00:00
*/
2025-06-30 19:19:46 +00:00
const handleSaveCollection = async (collectionData: Partial<Collection>) => {
2025-06-30 18:46:53 +00:00
try {
2025-06-30 19:19:46 +00:00
const isCreating = createModal()
const mutation = isCreating ? CREATE_COLLECTION_MUTATION : UPDATE_COLLECTION_MUTATION
2025-06-30 18:46:53 +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: { collection_input: collectionData }
2025-06-30 18:46:53 +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_collection : result.data.update_collection
if (resultData.error) {
throw new Error(resultData.error)
2025-06-30 18:46:53 +00:00
}
2025-06-30 19:19:46 +00:00
props.onSuccess(isCreating ? 'Коллекция успешно создана' : 'Коллекция успешно обновлена')
setCreateModal(false)
2025-06-30 18:46:53 +00:00
setEditModal({ show: false, collection: null })
await loadCollections()
} catch (error) {
2025-06-30 19:19:46 +00:00
props.onError(
`Ошибка ${createModal() ? 'создания' : 'обновления'} коллекции: ${(error as Error).message}`
)
2025-06-30 18:46:53 +00:00
}
}
/**
* Удаляет коллекцию
*/
const deleteCollection = async (slug: string) => {
try {
const response = await fetch('/graphql', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
query: DELETE_COLLECTION_MUTATION,
variables: { slug }
})
})
const result = await response.json()
if (result.errors) {
throw new Error(result.errors[0].message)
}
if (result.data.delete_collection.error) {
throw new Error(result.data.delete_collection.error)
}
props.onSuccess('Коллекция успешно удалена')
setDeleteModal({ show: false, collection: null })
await loadCollections()
} catch (error) {
props.onError(`Ошибка удаления коллекции: ${(error as Error).message}`)
}
}
// Загружаем коллекции при монтировании компонента
onMount(() => {
void loadCollections()
2025-07-02 19:30:21 +00:00
setFilteredCollections(collections())
2025-06-30 18:46:53 +00:00
})
return (
<div class={styles.container}>
2025-07-02 19:30:21 +00:00
<TableControls
isLoading={loading()}
searchValue={searchQuery()}
onSearchChange={handleSearchChange}
onSearch={handleSearch}
searchPlaceholder="Поиск по названию, slug или ID..."
actions={
<button class={`${styles.button} ${styles.primary}`} onClick={openCreateModal}>
2025-06-30 18:46:53 +00:00
Создать коллекцию
2025-07-02 19:30:21 +00:00
</button>
}
/>
2025-06-30 18:46:53 +00:00
<Show
when={!loading()}
fallback={
<div class="loading-screen">
<div class="loading-spinner" />
<div>Загрузка коллекций...</div>
</div>
}
>
<table class={styles.table}>
<thead>
<tr>
<th>ID</th>
<th>Название</th>
<th>Slug</th>
<th>Описание</th>
<th>Создатель</th>
<th>Публикации</th>
<th>Создано</th>
<th>Опубликовано</th>
<th>Действия</th>
</tr>
</thead>
<tbody>
2025-07-02 19:30:21 +00:00
<For each={filteredCollections()}>
2025-06-30 18:46:53 +00:00
{(collection) => (
<tr
onClick={() => openEditModal(collection)}
style={{ cursor: 'pointer' }}
class={styles['clickable-row']}
>
<td>{collection.id}</td>
<td>{collection.title}</td>
<td>{collection.slug}</td>
<td>
<div
style={{
'max-width': '200px',
overflow: 'hidden',
'text-overflow': 'ellipsis',
'white-space': 'nowrap'
}}
title={collection.desc}
>
{collection.desc || '—'}
</div>
</td>
<td>{collection.created_by.name || collection.created_by.email}</td>
<td>{collection.amount}</td>
<td>{formatDate(collection.created_at)}</td>
<td>{collection.published_at ? formatDate(collection.published_at) : '—'}</td>
<td onClick={(e) => e.stopPropagation()}>
<button
onClick={(e) => {
e.stopPropagation()
setDeleteModal({ show: true, collection })
}}
class={styles['delete-button']}
title="Удалить коллекцию"
aria-label="Удалить коллекцию"
>
×
</button>
</td>
</tr>
)}
</For>
</tbody>
</table>
</Show>
{/* Модальное окно создания */}
2025-06-30 19:19:46 +00:00
<CollectionEditModal
isOpen={createModal()}
collection={null}
onClose={() => setCreateModal(false)}
onSave={handleSaveCollection}
/>
2025-06-30 18:46:53 +00:00
{/* Модальное окно редактирования */}
2025-06-30 19:19:46 +00:00
<CollectionEditModal
2025-06-30 18:46:53 +00:00
isOpen={editModal().show}
2025-06-30 19:19:46 +00:00
collection={editModal().collection}
2025-06-30 18:46:53 +00:00
onClose={() => setEditModal({ show: false, collection: null })}
2025-06-30 19:19:46 +00:00
onSave={handleSaveCollection}
/>
2025-06-30 18:46:53 +00:00
{/* Модальное окно подтверждения удаления */}
<Modal
isOpen={deleteModal().show}
onClose={() => setDeleteModal({ show: false, collection: null })}
title="Подтверждение удаления"
>
<div>
<p>
Вы уверены, что хотите удалить коллекцию "<strong>{deleteModal().collection?.title}</strong>"?
</p>
<p class={styles['warning-text']}>
Это действие нельзя отменить. Все связи с публикациями будут удалены.
</p>
<div class={styles['modal-actions']}>
<Button variant="secondary" onClick={() => setDeleteModal({ show: false, collection: null })}>
Отмена
</Button>
<Button
variant="danger"
onClick={() => deleteModal().collection && deleteCollection(deleteModal().collection!.slug)}
>
Удалить
</Button>
</div>
</div>
</Modal>
</div>
)
}
export default CollectionsRoute