myYouTube/frontend/src/components/ChannelCard.tsx

101 lines
3.6 KiB
TypeScript
Raw Normal View History

import { useEffect, useRef, useState } from 'react'
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { Link } from 'react-router-dom'
import type { CategoryDto, ChannelDto } from '../api/client'
import { setChannelCategories } from '../api/client'
interface Props {
channel: ChannelDto
categories: CategoryDto[]
}
function ChannelCard({ channel, categories }: Props) {
const [open, setOpen] = useState(false)
const popoverRef = useRef<HTMLDivElement>(null)
const queryClient = useQueryClient()
useEffect(() => {
if (!open) return
function handleClickOutside(e: MouseEvent) {
if (popoverRef.current && !popoverRef.current.contains(e.target as Node)) {
setOpen(false)
}
}
document.addEventListener('mousedown', handleClickOutside)
return () => document.removeEventListener('mousedown', handleClickOutside)
}, [open])
const mutation = useMutation({
mutationFn: (categoryIds: number[]) => setChannelCategories(channel.id, categoryIds),
onMutate: async (categoryIds: number[]) => {
await queryClient.cancelQueries({ queryKey: ['channels'] })
const previous = queryClient.getQueriesData<ChannelDto[]>({ queryKey: ['channels'] })
queryClient.setQueriesData<ChannelDto[]>({ queryKey: ['channels'] }, (old) =>
old?.map((c) => (c.id === channel.id ? { ...c, category_ids: categoryIds } : c)),
)
return { previous }
},
onError: (_err, _vars, context) => {
context?.previous.forEach(([key, data]) => queryClient.setQueryData(key, data))
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['channels'] })
queryClient.invalidateQueries({ queryKey: ['categories'] })
},
})
function toggleCategory(categoryId: number) {
const current = new Set(channel.category_ids)
if (current.has(categoryId)) {
current.delete(categoryId)
} else {
current.add(categoryId)
}
mutation.mutate(Array.from(current))
}
const assignedNames = categories.filter((c) => channel.category_ids.includes(c.id)).map((c) => c.name)
return (
<li className="channel-card">
{channel.thumbnail_url && <img src={channel.thumbnail_url} alt={channel.title} />}
<div className="channel-info">
<Link to={`/channels/${channel.id}/videos`} className="channel-title">
{channel.title}
</Link>
{!channel.subscribed && <span className="badge">отписан</span>}
<div className="channel-categories">
{assignedNames.map((name) => (
<span key={name} className="chip">
{name}
</span>
))}
<div className="category-popover-wrapper" ref={popoverRef}>
<button className="chip-edit" onClick={() => setOpen((v) => !v)}>
+ Категория
</button>
{open && (
<div className="category-popover">
{categories.length === 0 && <div className="category-popover-empty">Сначала создай категорию</div>}
{categories.map((category) => (
<label key={category.id} className="category-checkbox">
<input
type="checkbox"
checked={channel.category_ids.includes(category.id)}
onChange={() => toggleCategory(category.id)}
/>
{category.name}
</label>
))}
</div>
)}
</div>
</div>
</div>
</li>
)
}
export default ChannelCard