Implemented new API endpoints for suggesting groups based on the current library, allowing admins to retrieve and create groups from suggestions. Updated the backend to include error handling for suggestions and added necessary DTOs. Enhanced the frontend with new API functions and UI components to display suggestions, improving the user experience for group management. Localization updates were made to support new features in both English and Russian.
98 lines
4.2 KiB
TypeScript
98 lines
4.2 KiB
TypeScript
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
|
import { ChevronDown, ChevronRight, Plus, Wand2 } from 'lucide-react'
|
|
import { useState } from 'react'
|
|
import { useTranslation } from 'react-i18next'
|
|
import { qk } from '@/shared/api/query-keys'
|
|
import type { GroupSuggestionDto } from '@/shared/api/types'
|
|
import { useApiError } from '@/shared/lib/use-api-error'
|
|
import { Badge } from '@/shared/ui/badge'
|
|
import { Button } from '@/shared/ui/button'
|
|
import { toast } from '@/shared/ui/toast-store'
|
|
import { createGroupFromSuggestion, suggestGroups } from './api'
|
|
import { DurationLabel } from './DurationLabel'
|
|
|
|
/**
|
|
* Что имеет смысл завести группой при нынешней библиотеке. Разбор делает сервер (жанры, типы,
|
|
* рейтинги, крупные сериалы), здесь — только показ и кнопка. Уже созданное остаётся в списке
|
|
* помеченным: исчезнувшее без следа предложение читается как сбой, а «уже есть» объясняет,
|
|
* почему кнопки нет.
|
|
*/
|
|
export function GroupSuggestions() {
|
|
const { t } = useTranslation()
|
|
const queryClient = useQueryClient()
|
|
const onError = useApiError()
|
|
const [collapsed, setCollapsed] = useState(false)
|
|
|
|
const { data, isLoading } = useQuery({
|
|
queryKey: qk.groups.suggestions,
|
|
queryFn: suggestGroups,
|
|
})
|
|
|
|
const create = useMutation({
|
|
mutationFn: (suggestion: GroupSuggestionDto) => createGroupFromSuggestion(suggestion.key),
|
|
onSuccess: () => {
|
|
toast.success(t('admin.groups.suggestions.created'))
|
|
void queryClient.invalidateQueries({ queryKey: qk.groups.all })
|
|
},
|
|
onError,
|
|
})
|
|
|
|
// Пока предлагать нечего (пустая библиотека или всё уже создано) — секции нет вовсе.
|
|
if (isLoading || !data || data.length === 0) return null
|
|
|
|
return (
|
|
<div className="flex flex-col gap-2">
|
|
<div className="flex flex-wrap items-center gap-2">
|
|
<button
|
|
type="button"
|
|
className="flex items-center gap-2 text-sm font-semibold uppercase tracking-wide text-muted-foreground hover:text-foreground"
|
|
onClick={() => setCollapsed((v) => !v)}
|
|
>
|
|
{collapsed ? <ChevronRight className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
|
|
<Wand2 className="h-4 w-4" />
|
|
{t('admin.groups.suggestions.title')}
|
|
<Badge variant="muted">{data.filter((s) => !s.alreadyExists).length}</Badge>
|
|
</button>
|
|
</div>
|
|
|
|
{!collapsed && (
|
|
<>
|
|
<p className="text-xs text-muted-foreground">{t('admin.groups.suggestions.hint')}</p>
|
|
<ul className="crt-panel divide-y divide-border rounded-md text-sm">
|
|
{data.map((suggestion) => (
|
|
<li key={suggestion.key} className="flex flex-wrap items-center gap-2 px-4 py-2">
|
|
<span className="min-w-0 flex-1 truncate font-medium">{suggestion.name}</span>
|
|
<Badge variant="muted">
|
|
{t(`admin.groups.suggestions.kinds.${suggestion.kind}`)}
|
|
</Badge>
|
|
<span className="text-muted-foreground">
|
|
{t('admin.groups.suggestions.stats', {
|
|
shows: suggestion.showCount,
|
|
units: suggestion.unitCount,
|
|
})}
|
|
</span>
|
|
<span className="text-muted-foreground">
|
|
<DurationLabel seconds={suggestion.totalDurationSeconds} />
|
|
</span>
|
|
{suggestion.alreadyExists ? (
|
|
<Badge variant="muted">{t('admin.groups.suggestions.exists')}</Badge>
|
|
) : (
|
|
<Button
|
|
size="sm"
|
|
variant="outline"
|
|
disabled={create.isPending}
|
|
onClick={() => create.mutate(suggestion)}
|
|
>
|
|
<Plus className="h-4 w-4" />
|
|
{t('common.create')}
|
|
</Button>
|
|
)}
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|