146 lines
4.6 KiB
TypeScript
146 lines
4.6 KiB
TypeScript
|
|
import { useState } from 'react'
|
|||
|
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
|||
|
|
import { Link } from 'react-router-dom'
|
|||
|
|
import {
|
|||
|
|
createCategory,
|
|||
|
|
deleteCategory,
|
|||
|
|
listCategories,
|
|||
|
|
reorderCategories,
|
|||
|
|
updateCategory,
|
|||
|
|
} from '../api/client'
|
|||
|
|
|
|||
|
|
function Categories() {
|
|||
|
|
const queryClient = useQueryClient()
|
|||
|
|
const [newName, setNewName] = useState('')
|
|||
|
|
const [editingId, setEditingId] = useState<number | null>(null)
|
|||
|
|
const [editingName, setEditingName] = useState('')
|
|||
|
|
|
|||
|
|
const categoriesQuery = useQuery({ queryKey: ['categories'], queryFn: listCategories })
|
|||
|
|
|
|||
|
|
const invalidate = () => {
|
|||
|
|
queryClient.invalidateQueries({ queryKey: ['categories'] })
|
|||
|
|
queryClient.invalidateQueries({ queryKey: ['channels'] })
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const createMutation = useMutation({
|
|||
|
|
mutationFn: (name: string) => createCategory(name),
|
|||
|
|
onSuccess: () => {
|
|||
|
|
setNewName('')
|
|||
|
|
invalidate()
|
|||
|
|
},
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
const updateMutation = useMutation({
|
|||
|
|
mutationFn: ({ id, name }: { id: number; name: string }) => updateCategory(id, name),
|
|||
|
|
onSuccess: () => {
|
|||
|
|
setEditingId(null)
|
|||
|
|
invalidate()
|
|||
|
|
},
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
const deleteMutation = useMutation({
|
|||
|
|
mutationFn: (id: number) => deleteCategory(id),
|
|||
|
|
onSuccess: invalidate,
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
const reorderMutation = useMutation({
|
|||
|
|
mutationFn: (ids: number[]) => reorderCategories(ids),
|
|||
|
|
onSuccess: invalidate,
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
const categories = categoriesQuery.data ?? []
|
|||
|
|
|
|||
|
|
function move(index: number, direction: -1 | 1) {
|
|||
|
|
const target = index + direction
|
|||
|
|
if (target < 0 || target >= categories.length) return
|
|||
|
|
const ids = categories.map((c) => c.id)
|
|||
|
|
;[ids[index], ids[target]] = [ids[target], ids[index]]
|
|||
|
|
reorderMutation.mutate(ids)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<div className="categories-screen">
|
|||
|
|
<Link to="/" className="back-link">
|
|||
|
|
← Каналы
|
|||
|
|
</Link>
|
|||
|
|
<h1>Категории</h1>
|
|||
|
|
|
|||
|
|
<form
|
|||
|
|
onSubmit={(e) => {
|
|||
|
|
e.preventDefault()
|
|||
|
|
if (newName.trim()) createMutation.mutate(newName.trim())
|
|||
|
|
}}
|
|||
|
|
className="category-form"
|
|||
|
|
>
|
|||
|
|
<input
|
|||
|
|
type="text"
|
|||
|
|
placeholder="Новая категория"
|
|||
|
|
value={newName}
|
|||
|
|
onChange={(e) => setNewName(e.target.value)}
|
|||
|
|
/>
|
|||
|
|
<button type="submit" disabled={!newName.trim() || createMutation.isPending}>
|
|||
|
|
Создать
|
|||
|
|
</button>
|
|||
|
|
</form>
|
|||
|
|
{createMutation.isError && <p className="error">{(createMutation.error as Error).message}</p>}
|
|||
|
|
|
|||
|
|
<ul className="category-list">
|
|||
|
|
{categories.map((category, index) => (
|
|||
|
|
<li key={category.id} className="category-row">
|
|||
|
|
<div className="reorder-buttons">
|
|||
|
|
<button onClick={() => move(index, -1)} disabled={index === 0} aria-label="Выше">
|
|||
|
|
↑
|
|||
|
|
</button>
|
|||
|
|
<button onClick={() => move(index, 1)} disabled={index === categories.length - 1} aria-label="Ниже">
|
|||
|
|
↓
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{editingId === category.id ? (
|
|||
|
|
<form
|
|||
|
|
className="category-edit-form"
|
|||
|
|
onSubmit={(e) => {
|
|||
|
|
e.preventDefault()
|
|||
|
|
if (editingName.trim()) updateMutation.mutate({ id: category.id, name: editingName.trim() })
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
<input value={editingName} onChange={(e) => setEditingName(e.target.value)} autoFocus />
|
|||
|
|
<button type="submit">Сохранить</button>
|
|||
|
|
<button type="button" onClick={() => setEditingId(null)}>
|
|||
|
|
Отмена
|
|||
|
|
</button>
|
|||
|
|
</form>
|
|||
|
|
) : (
|
|||
|
|
<>
|
|||
|
|
<span className="category-name">{category.name}</span>
|
|||
|
|
<span className="category-count">{category.channel_count} канал(ов)</span>
|
|||
|
|
<button
|
|||
|
|
onClick={() => {
|
|||
|
|
setEditingId(category.id)
|
|||
|
|
setEditingName(category.name)
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
Переименовать
|
|||
|
|
</button>
|
|||
|
|
<button
|
|||
|
|
onClick={() => {
|
|||
|
|
if (confirm(`Удалить категорию "${category.name}"? Каналы останутся.`)) {
|
|||
|
|
deleteMutation.mutate(category.id)
|
|||
|
|
}
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
Удалить
|
|||
|
|
</button>
|
|||
|
|
</>
|
|||
|
|
)}
|
|||
|
|
</li>
|
|||
|
|
))}
|
|||
|
|
</ul>
|
|||
|
|
|
|||
|
|
{categories.length === 0 && !categoriesQuery.isLoading && <p>Категорий пока нет.</p>}
|
|||
|
|
</div>
|
|||
|
|
)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export default Categories
|