Refactor ShowAudience handling across the application to align with MPAA rating system. Update ShowAudience enum to reflect new ratings (G, PG, PG-13, R, NC-17) and adjust related data models, API types, and frontend components to support nullable audience values. Enhance metadata handling to incorporate content ratings from external sources, ensuring proper audience filtering in scheduling logic. Update documentation to clarify changes in audience categorization and its implications for content management.
This commit is contained in:
@@ -15,7 +15,7 @@ import { Label } from '@/shared/ui/label'
|
||||
import { updateTemplate } from '../api'
|
||||
import { CollapsibleCard } from './CollapsibleCard'
|
||||
|
||||
const EMPTY_WINDOW: AudienceWindow = { from: '06:00:00', to: '23:00:00', maxAudience: 'Teen' }
|
||||
const EMPTY_WINDOW: AudienceWindow = { from: '06:00:00', to: '23:00:00', maxAudience: 'PG-13' }
|
||||
|
||||
/**
|
||||
* Правила отбора кандидатов канала (см. 3.8): детское время и потолок повторов. Это жёсткие
|
||||
|
||||
@@ -4,7 +4,12 @@ import { useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { ChevronLeft } from 'lucide-react'
|
||||
import { qk } from '@/shared/api/query-keys'
|
||||
import { SHOW_AUDIENCES, type MediaAssetDto, type ShowAudience } from '@/shared/api/types'
|
||||
import {
|
||||
AUDIENCE_UNSET,
|
||||
SHOW_AUDIENCES,
|
||||
type MediaAssetDto,
|
||||
type ShowAudience,
|
||||
} from '@/shared/api/types'
|
||||
import { useApiError } from '@/shared/lib/use-api-error'
|
||||
import { Badge } from '@/shared/ui/badge'
|
||||
import { Button } from '@/shared/ui/button'
|
||||
@@ -51,7 +56,7 @@ export function ShowDetail({ showId }: { showId: string }) {
|
||||
const onError = useApiError()
|
||||
|
||||
const audienceMutation = useMutation({
|
||||
mutationFn: (audience: ShowAudience) => setShowAudience(showId, audience),
|
||||
mutationFn: (audience: ShowAudience | null) => setShowAudience(showId, audience),
|
||||
onSuccess: invalidate,
|
||||
onError,
|
||||
})
|
||||
@@ -150,13 +155,16 @@ export function ShowDetail({ showId }: { showId: string }) {
|
||||
<h2 className="crt-glow text-xl font-semibold">{show.name}</h2>
|
||||
<Badge variant="muted">{t(`admin.shows.kinds.${show.kind}`)}</Badge>
|
||||
<Select
|
||||
value={show.audience}
|
||||
onValueChange={(v) => audienceMutation.mutate(v as ShowAudience)}
|
||||
value={show.audience ?? AUDIENCE_UNSET}
|
||||
onValueChange={(v) =>
|
||||
audienceMutation.mutate(v === AUDIENCE_UNSET ? null : (v as ShowAudience))
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="h-7 w-32">
|
||||
<SelectTrigger className="h-7 w-48">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value={AUDIENCE_UNSET}>{t('admin.shows.audienceUnset')}</SelectItem>
|
||||
{SHOW_AUDIENCES.map((value) => (
|
||||
<SelectItem key={value} value={value}>
|
||||
{t(`admin.shows.audiences.${value}`)}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Loader2 } from 'lucide-react'
|
||||
import { qk } from '@/shared/api/query-keys'
|
||||
import type { MetadataCandidate, MissingEpisodesReport, ShowDto } 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 { Card, CardContent, CardHeader, CardTitle } from '@/shared/ui/card'
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/shared/ui/dialog'
|
||||
@@ -199,10 +200,17 @@ export function ShowMetadataCard({ show, onChanged }: { show: ShowDto; onChanged
|
||||
<div className="h-16 w-11 shrink-0 rounded bg-muted/40" />
|
||||
)}
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="text-sm font-medium">
|
||||
{r.title}
|
||||
{r.year != null && (
|
||||
<span className="text-muted-foreground"> ({r.year})</span>
|
||||
<div className="flex flex-wrap items-center gap-2 text-sm font-medium">
|
||||
<span>
|
||||
{r.title}
|
||||
{r.year != null && (
|
||||
<span className="text-muted-foreground"> ({r.year})</span>
|
||||
)}
|
||||
</span>
|
||||
{/* В выдаче OMDb сериалы и полнометражки идут вперемешку — без метки
|
||||
одноимённые фильм и сериал не различить. */}
|
||||
{r.kind && (
|
||||
<Badge variant="muted">{t(`admin.shows.kinds.${r.kind}`)}</Badge>
|
||||
)}
|
||||
</div>
|
||||
{r.overview && (
|
||||
|
||||
@@ -3,7 +3,12 @@ import { Link } from '@tanstack/react-router'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { qk } from '@/shared/api/query-keys'
|
||||
import { SHOW_AUDIENCES, type ShowAudience, type ShowKind } from '@/shared/api/types'
|
||||
import {
|
||||
AUDIENCE_UNSET,
|
||||
SHOW_AUDIENCES,
|
||||
type ShowAudience,
|
||||
type ShowKind,
|
||||
} from '@/shared/api/types'
|
||||
import { useApiError } from '@/shared/lib/use-api-error'
|
||||
import { Badge } from '@/shared/ui/badge'
|
||||
import { Button } from '@/shared/ui/button'
|
||||
@@ -22,7 +27,8 @@ export function ShowsPanel() {
|
||||
const [name, setName] = useState('')
|
||||
const [originalName, setOriginalName] = useState('')
|
||||
const [kind, setKind] = useState<ShowKind>('Series')
|
||||
const [audience, setAudience] = useState<ShowAudience>('General')
|
||||
// Новое шоу заводится без рейтинга: проставят метаданные либо админ руками.
|
||||
const [audience, setAudience] = useState<ShowAudience | null>(null)
|
||||
const [query, setQuery] = useState('')
|
||||
const [page, setPage] = useState(1)
|
||||
const { sort, toggle } = useTableSort('name', false)
|
||||
@@ -52,7 +58,8 @@ export function ShowsPanel() {
|
||||
return sortRows(matched, sort, {
|
||||
name: (s) => s.name.toLowerCase(),
|
||||
kind: (s) => s.kind,
|
||||
audience: (s) => s.audience,
|
||||
// Без рейтинга — в начало: пустое значение сортируется раньше любого кода.
|
||||
audience: (s) => s.audience ?? '',
|
||||
genre: (s) => (s.primaryGenre ?? '').toLowerCase(),
|
||||
seasons: (s) => s.seasonCount,
|
||||
episodes: (s) => s.episodeCount,
|
||||
@@ -106,11 +113,15 @@ export function ShowsPanel() {
|
||||
<SelectItem value="Single">{t('admin.shows.kinds.Single')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Select value={audience} onValueChange={(v) => setAudience(v as ShowAudience)}>
|
||||
<SelectTrigger className="w-40">
|
||||
<Select
|
||||
value={audience ?? AUDIENCE_UNSET}
|
||||
onValueChange={(v) => setAudience(v === AUDIENCE_UNSET ? null : (v as ShowAudience))}
|
||||
>
|
||||
<SelectTrigger className="w-48">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value={AUDIENCE_UNSET}>{t('admin.shows.audienceUnset')}</SelectItem>
|
||||
{SHOW_AUDIENCES.map((value) => (
|
||||
<SelectItem key={value} value={value}>
|
||||
{t(`admin.shows.audiences.${value}`)}
|
||||
@@ -224,7 +235,13 @@ export function ShowsPanel() {
|
||||
<Badge variant="muted">{t(`admin.shows.kinds.${show.kind}`)}</Badge>
|
||||
</td>
|
||||
<td className="px-4 py-2">
|
||||
<Badge variant="muted">{t(`admin.shows.audiences.${show.audience}`)}</Badge>
|
||||
{/* В таблице показываем сам код MPAA: он короткий и одинаков во всех локалях,
|
||||
а расшифровка есть в селекте на карточке шоу. */}
|
||||
{show.audience ? (
|
||||
<Badge variant="muted">{show.audience}</Badge>
|
||||
) : (
|
||||
<span className="text-muted-foreground">—</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-2 text-muted-foreground">{show.primaryGenre ?? '—'}</td>
|
||||
<td className="px-4 py-2 text-muted-foreground">{show.seasonCount}</td>
|
||||
|
||||
@@ -23,12 +23,13 @@ export function createShow(body: {
|
||||
kind: ShowKind
|
||||
description?: string
|
||||
originalName?: string
|
||||
audience?: ShowAudience
|
||||
audience?: ShowAudience | null
|
||||
}) {
|
||||
return apiRequest<CreatedIdResponse>('/admin/shows', { method: 'POST', body })
|
||||
}
|
||||
|
||||
export function setShowAudience(id: string, audience: ShowAudience) {
|
||||
/** `null` — снять проставленный рейтинг. */
|
||||
export function setShowAudience(id: string, audience: ShowAudience | null) {
|
||||
return apiRequest<void>(`/admin/shows/${id}/audience`, { method: 'PUT', body: { audience } })
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user