Same "channels per category" counts as the categories management page, now next to each button in the sidebar used across all three pages. Categories already carry channel_count; "Все"/"Без категории" counts are derived client-side from one extra unfiltered channels query, reused across the three pages via a shared ['channels', 'all-for-counts'] key. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
121 lines
4.4 KiB
TypeScript
121 lines
4.4 KiB
TypeScript
import { useMemo, useState } from 'react'
|
||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||
import { Link } from 'react-router-dom'
|
||
import { getSyncStatus, listCategories, listChannels, logout, syncSubscriptions } from '../api/client'
|
||
import ChannelCard from '../components/ChannelCard'
|
||
import CategoryNav, { type CategoryFilter } from '../components/CategoryNav'
|
||
|
||
function Channels() {
|
||
const [search, setSearch] = useState('')
|
||
const [filter, setFilter] = useState<CategoryFilter>('all')
|
||
const queryClient = useQueryClient()
|
||
|
||
const categoriesQuery = useQuery({ queryKey: ['categories'], queryFn: listCategories })
|
||
|
||
const allChannelsQuery = useQuery({
|
||
queryKey: ['channels', 'all-for-counts'],
|
||
queryFn: () => listChannels({ subscribed: true }),
|
||
})
|
||
const allChannels = allChannelsQuery.data ?? []
|
||
const uncategorizedCount = allChannels.filter((c) => c.category_ids.length === 0).length
|
||
|
||
const channelsQuery = useQuery({
|
||
queryKey: ['channels', filter],
|
||
queryFn: () =>
|
||
listChannels({
|
||
subscribed: true,
|
||
categoryId: typeof filter === 'number' ? filter : undefined,
|
||
uncategorized: filter === 'uncategorized',
|
||
}),
|
||
})
|
||
|
||
const filteredChannels = useMemo(() => {
|
||
const data = channelsQuery.data ?? []
|
||
if (!search.trim()) return data
|
||
const needle = search.trim().toLowerCase()
|
||
return data.filter((c) => c.title.toLowerCase().includes(needle))
|
||
}, [channelsQuery.data, search])
|
||
|
||
const syncStatusQuery = useQuery({
|
||
queryKey: ['sync-status'],
|
||
queryFn: getSyncStatus,
|
||
refetchInterval: (query) => (query.state.data?.subscriptions.running ? 1500 : false),
|
||
})
|
||
|
||
const syncMutation = useMutation({
|
||
mutationFn: syncSubscriptions,
|
||
onSuccess: () => {
|
||
queryClient.invalidateQueries({ queryKey: ['channels'] })
|
||
queryClient.invalidateQueries({ queryKey: ['sync-status'] })
|
||
},
|
||
})
|
||
|
||
const logoutMutation = useMutation({
|
||
mutationFn: logout,
|
||
onSuccess: () => {
|
||
queryClient.invalidateQueries({ queryKey: ['auth-status'] })
|
||
},
|
||
})
|
||
|
||
const running = syncStatusQuery.data?.subscriptions.running
|
||
const categories = categoriesQuery.data ?? []
|
||
|
||
return (
|
||
<div className="channels-screen">
|
||
<header>
|
||
<h1>Каналы</h1>
|
||
<div className="actions">
|
||
<Link to="/">Лента</Link>
|
||
<Link to="/saved">Сохранённые</Link>
|
||
<Link to="/categories">Категории</Link>
|
||
<button onClick={() => syncMutation.mutate()} disabled={running}>
|
||
{running ? 'Синхронизация...' : 'Обновить сейчас'}
|
||
</button>
|
||
<button onClick={() => logoutMutation.mutate()}>Выйти</button>
|
||
</div>
|
||
</header>
|
||
|
||
{syncMutation.isError && <p className="error">{(syncMutation.error as Error).message}</p>}
|
||
{syncStatusQuery.data?.subscriptions.status === 'failed' && (
|
||
<p className="error">Последняя синхронизация завершилась ошибкой: {syncStatusQuery.data.subscriptions.error}</p>
|
||
)}
|
||
|
||
<div className="layout">
|
||
<CategoryNav
|
||
categories={categories}
|
||
value={filter}
|
||
onChange={setFilter}
|
||
allCount={allChannels.length}
|
||
uncategorizedCount={uncategorizedCount}
|
||
/>
|
||
|
||
<div className="content">
|
||
<input
|
||
type="text"
|
||
placeholder="Поиск по названию канала"
|
||
value={search}
|
||
onChange={(e) => setSearch(e.target.value)}
|
||
/>
|
||
|
||
{channelsQuery.isLoading && <p>Загрузка...</p>}
|
||
{channelsQuery.isError && <p className="error">Не удалось загрузить каналы</p>}
|
||
|
||
<ul className="channel-list">
|
||
{filteredChannels.map((channel) => (
|
||
<ChannelCard key={channel.id} channel={channel} categories={categories} />
|
||
))}
|
||
</ul>
|
||
|
||
{channelsQuery.data && channelsQuery.data.length === 0 && (
|
||
<p>Каналы не найдены{filter === 'all' ? '. Нажми "Обновить сейчас".' : ' в этой категории.'}</p>
|
||
)}
|
||
{channelsQuery.data && channelsQuery.data.length > 0 && filteredChannels.length === 0 && (
|
||
<p>Ничего не найдено по запросу "{search}".</p>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
export default Channels
|