Refactor components to enhance code clarity and maintainability by updating prop types to use Readonly for better immutability. Adjust Result class methods for consistency, and streamline query functions in ChannelDetail and other components to improve performance and readability.
ci / build-backend (push) Successful in 1m41s
ci / build-frontend (push) Successful in 47s
ci / tests (push) Successful in 2m40s
ci / sonar (push) Successful in 5m35s

This commit is contained in:
Leonid Pershin
2026-07-27 01:14:53 +03:00
parent ecb5417170
commit 5449c05b5b
56 changed files with 132 additions and 124 deletions
@@ -31,7 +31,7 @@ import { ViewerCard } from './components/ViewerCard'
const TABS = ['settings', 'grid', 'rules', 'junctions', 'bumpers', 'viewer', 'air'] as const
type ChannelTab = (typeof TABS)[number]
export function ChannelDetail({ channelId }: { channelId: string }) {
export function ChannelDetail({ channelId }: Readonly<{ channelId: string }>) {
const { t } = useTranslation()
const queryClient = useQueryClient()
const [applyOpen, setApplyOpen] = useState(false)
@@ -52,7 +52,7 @@ export function ChannelDetail({ channelId }: { channelId: string }) {
})
const { data: schedule } = useQuery({
queryKey: qk.channels.schedule(channelId),
queryFn: () => getSchedule(channelId, new Date(), new Date(Date.now() + 12 * 3600_000)),
queryFn: () => getSchedule(channelId, new Date(), new Date(Date.now() + 12 * 3_600_000)),
})
const invalidate = () => {
@@ -25,13 +25,13 @@ export function ApplyDialog({
pending,
onApply,
onClose,
}: {
}: Readonly<{
channelId: string
utcOffsetMinutes: number
pending: boolean
onApply: () => void
onClose: () => void
}) {
}>) {
const { t } = useTranslation()
const { data, isFetching } = useQuery({
queryKey: qk.channels.diff(channelId),
@@ -13,13 +13,13 @@ export function BumperBackgroundField({
backgroundImageId,
onChanged,
onError,
}: {
}: Readonly<{
channelId: string
templateId: string
backgroundImageId: string | null
onChanged: () => void
onError: (e: unknown) => void
}) {
}>) {
const { t } = useTranslation()
const [galleryOpen, setGalleryOpen] = useState(false)
@@ -15,12 +15,12 @@ export function BumperCard({
bare,
onSaved,
onError,
}: {
}: Readonly<{
channel: ChannelDto
bare?: boolean
onSaved: () => void
onError: (e: unknown) => void
}) {
}>) {
const { t } = useTranslation()
const [bumpersEnabled, setBumpersEnabled] = useState(channel.bumpersEnabled)
const [bumper, setBumper] = useState<BumperSettings>(channel.bumper)
@@ -15,7 +15,7 @@ export function BumperFileUpload({
clear,
onSaved,
onError,
}: {
}: Readonly<{
channelId: string
templateId: string
kind: string
@@ -27,7 +27,7 @@ export function BumperFileUpload({
clear: (id: string, templateId: string) => Promise<void>
onSaved: () => void
onError: (e: unknown) => void
}) {
}>) {
const { t } = useTranslation()
const inputId = `bumper-${kind}-${templateId}`
@@ -11,12 +11,12 @@ export function BumperPreviewPlayer({
templateId,
variants,
onError,
}: {
}: Readonly<{
channelId: string
templateId: string
variants: BumperTextVariantDto[]
onError: (e: unknown) => void
}) {
}>) {
const { t } = useTranslation()
const [ready, setReady] = useState(false)
const [bust, setBust] = useState(0)
@@ -26,12 +26,12 @@ export function BumperTemplateEditor({
template,
onChanged,
onError,
}: {
}: Readonly<{
channelId: string
template: BumperTemplateDto
onChanged: () => void
onError: (e: unknown) => void
}) {
}>) {
const { t } = useTranslation()
const [open, setOpen] = useState(false)
const [name, setName] = useState(template.name)
@@ -16,14 +16,14 @@ export function BumperVariantEditor({
canRemove,
onChanged,
onError,
}: {
}: Readonly<{
channelId: string
templateId: string
variant: BumperTextVariantDto
canRemove: boolean
onChanged: () => void
onError: (e: unknown) => void
}) {
}>) {
const { t } = useTranslation()
const [form, setForm] = useState({
name: variant.name,
@@ -9,14 +9,14 @@ export function CollapsibleCard({
bare = false,
contentClassName,
children,
}: {
}: Readonly<{
title: string
defaultOpen?: boolean
/** Без своего заголовка и сворачивания — когда карточка и так лежит во вкладке с этим названием. */
bare?: boolean
contentClassName?: string
children: ReactNode
}) {
}>) {
const [open, setOpen] = useState(defaultOpen)
if (bare)
@@ -19,11 +19,11 @@ export function EntryTraceDialog({
entryId,
utcOffsetMinutes,
onClose,
}: {
}: Readonly<{
entryId: string
utcOffsetMinutes: number
onClose: () => void
}) {
}>) {
const { t } = useTranslation()
const { data } = useQuery({
queryKey: qk.entries.trace(entryId),
@@ -99,7 +99,7 @@ function strategySummary(data: EntryTraceDto, t: Translate) {
])
}
function Row({ label, children }: { label: string; children: React.ReactNode }) {
function Row({ label, children }: Readonly<{ label: string; children: React.ReactNode }>) {
return (
<>
<dt className="text-muted-foreground">{label}</dt>
@@ -38,13 +38,13 @@ export function GridTab({
templateError,
onChanged,
onError,
}: {
}: Readonly<{
channelId: string
template: ScheduleTemplateDto | undefined
templateError: unknown
onChanged: () => void
onError: (error: unknown) => void
}) {
}>) {
const { t } = useTranslation()
const [draft, setDraft] = useState<SlotDraft | null>(null)
const [activeLayerId, setActiveLayerId] = useState<string | null>(null)
@@ -141,17 +141,17 @@ export function GridTab({
slot,
weekday,
startMinutes,
}: {
}: Readonly<{
slot: SlotDto
weekday: number
startMinutes: number
}) => updateSlot(slot.id, { ...toSlotBody(slot), weekday, targetStart: toTime(startMinutes) }),
}>) => updateSlot(slot.id, { ...toSlotBody(slot), weekday, targetStart: toTime(startMinutes) }),
onSuccess: onChanged,
onError,
})
const resizeSlotMutation = useMutation({
mutationFn: ({ slot, minutes }: { slot: SlotDto; minutes: number }) =>
mutationFn: ({ slot, minutes }: Readonly<{ slot: SlotDto; minutes: number }>) =>
updateSlot(slot.id, { ...toSlotBody(slot), targetDurationMinutes: minutes }),
onSuccess: onChanged,
onError,
@@ -159,7 +159,7 @@ export function GridTab({
/** Копирование дня: слоты «каждый день» не копируются — они и так есть во всех колонках. */
const copyDayMutation = useMutation({
mutationFn: async ({ from, to }: { from: number; to: number[] }) => {
mutationFn: async ({ from, to }: Readonly<{ from: number; to: number[] }>) => {
const sources = (template?.layers ?? []).flatMap((layer) =>
layer.slots.filter((slot) => slot.weekday === from).map((slot) => ({ layer, slot })),
)
@@ -44,14 +44,14 @@ export function JunctionElementDialog({
onClose,
onChanged,
onError,
}: {
}: Readonly<{
junctionId: string
element: JunctionElementDto
bumperTemplates: BumperTemplateDto[]
onClose: () => void
onChanged: () => void
onError: (error: unknown) => void
}) {
}>) {
const { t } = useTranslation()
const [body, setBody] = useState<JunctionElementBody>(() => toBody(element))
const { data: groups } = useQuery({ queryKey: qk.groups.all, queryFn: listGroups })
@@ -86,13 +86,13 @@ export function JunctionsCard({
bare,
onChanged,
onError,
}: {
}: Readonly<{
channel: ChannelDto
template: ScheduleTemplateDto | undefined
bare?: boolean
onChanged: () => void
onError: (error: unknown) => void
}) {
}>) {
const { t } = useTranslation()
const [newName, setNewName] = useState('')
@@ -186,13 +186,13 @@ function JunctionChain({
groups,
onChanged,
onError,
}: {
}: Readonly<{
junction: JunctionTemplateDto
channel: ChannelDto
groups: GroupSummaryDto[] | undefined
onChanged: () => void
onError: (error: unknown) => void
}) {
}>) {
const { t } = useTranslation()
const [name, setName] = useState<string | null>(null)
const [dragged, setDragged] = useState<string | null>(null)
@@ -29,12 +29,12 @@ export function LayerApplicabilityDialog({
onClose,
onChanged,
onError,
}: {
}: Readonly<{
layer: GridLayerDto
onClose: () => void
onChanged: () => void
onError: (error: unknown) => void
}) {
}>) {
const { t } = useTranslation()
const [name, setName] = useState(layer.name)
const [weekdays, setWeekdays] = useState<number[]>(layer.applicability?.weekdays ?? [])
@@ -199,12 +199,12 @@ function Section({
onAdd,
empty,
children,
}: {
}: Readonly<{
title: string
onAdd: () => void
empty: boolean
children: React.ReactNode
}) {
}>) {
const { t } = useTranslation()
return (
<div className="flex flex-col gap-2">
@@ -228,11 +228,11 @@ function MonthDay({
value,
prefix,
onChange,
}: {
}: Readonly<{
value: AnnualRange
prefix: 'from' | 'to'
onChange: (part: Partial<AnnualRange>) => void
}) {
}>) {
const month = prefix === 'from' ? value.fromMonth : value.toMonth
const day = prefix === 'from' ? value.fromDay : value.toDay
@@ -37,12 +37,12 @@ export function RulesCard({
bare,
onChanged,
onError,
}: {
}: Readonly<{
template: ScheduleTemplateDto
bare?: boolean
onChanged: () => void
onError: (error: unknown) => void
}) {
}>) {
const { t } = useTranslation()
const [windows, setWindows] = useState<WindowRow[]>(() =>
toRows(template.rules?.maxAudienceByTime ?? []),
@@ -56,7 +56,7 @@ export function ScheduleGrid({
onMoveSlot,
onResizeSlot,
onCopyDay,
}: {
}: Readonly<{
template: ScheduleTemplateDto
selectedSlotId: string | null
/** Дата, на которую смотрим сетку («показать 25 декабря»); null — все слои разом. */
@@ -67,7 +67,7 @@ export function ScheduleGrid({
onMoveSlot: (slot: SlotDto, weekday: number, startMinutes: number) => void
onResizeSlot: (slot: SlotDto, durationMinutes: number) => void
onCopyDay: (fromWeekday: number) => void
}) {
}>) {
const { t } = useTranslation()
const dayStart = template.dayStartTime.slice(0, 5)
const dayStartMinutes = minutesOf(dayStart)
@@ -280,7 +280,7 @@ export function LayerList({
onToggle,
onReorder,
onEditApplicability,
}: {
}: Readonly<{
template: ScheduleTemplateDto
activeLayerId: string | null
viewDate: string | null
@@ -289,7 +289,7 @@ export function LayerList({
onToggle: (layer: GridLayerDto) => void
onReorder: (layerIdsTopFirst: string[]) => void
onEditApplicability: (layer: GridLayerDto) => void
}) {
}>) {
const { t } = useTranslation()
const [dragged, setDragged] = useState<string | null>(null)
@@ -5,7 +5,7 @@ import { Badge } from '@/shared/ui/badge'
import { formatTime } from '../lib/format'
/** Что стоит в строке расписания: реклама, заставка-переход или программа с номером серии. */
function EntryLabel({ entry }: { entry: ScheduleEntryDto }) {
function EntryLabel({ entry }: Readonly<{ entry: ScheduleEntryDto }>) {
const { t } = useTranslation()
if (entry.kind === 'Ad') return <Badge variant="muted">{t('air.ad')}</Badge>
@@ -35,7 +35,7 @@ function EntryLabel({ entry }: { entry: ScheduleEntryDto }) {
}
/** «· S02E05» либо «· серия N» — что удалось распознать; ничего, если ни того ни другого нет. */
function EpisodeSuffix({ entry }: { entry: ScheduleEntryDto }) {
function EpisodeSuffix({ entry }: Readonly<{ entry: ScheduleEntryDto }>) {
const { t } = useTranslation()
if (entry.seasonEpisode)
@@ -54,10 +54,10 @@ function EpisodeSuffix({ entry }: { entry: ScheduleEntryDto }) {
export function SchedulePreview({
entries,
onShowTrace,
}: {
}: Readonly<{
entries: ScheduleEntryDto[]
onShowTrace: (entryId: string) => void
}) {
}>) {
const { t } = useTranslation()
if (entries.length === 0)
return <p className="text-muted-foreground">{t('admin.channels.noSchedule')}</p>
@@ -16,13 +16,13 @@ export function SettingsCard({
bare,
onSaved,
onError,
}: {
}: Readonly<{
channel: ChannelDto
readyAssets: { id: string; originalFileName: string }[]
bare?: boolean
onSaved: () => void
onError: (e: unknown) => void
}) {
}>) {
const { t } = useTranslation()
const [name, setName] = useState(channel.name)
const [isEnabled, setIsEnabled] = useState(channel.isEnabled)
@@ -68,12 +68,12 @@ export function SlotInspector({
draft,
onClose,
onChanged,
}: {
}: Readonly<{
channelId: string
draft: SlotDraft
onClose: () => void
onChanged: () => void
}) {
}>) {
const { t } = useTranslation()
const [body, setBody] = useState<SlotBody>(() =>
draft.slot ? toSlotBody(draft.slot) : emptyBody(draft.defaults),
@@ -14,11 +14,11 @@ export function TemplateIssues({
channelId,
slotsById,
onGoToSlot,
}: {
}: Readonly<{
channelId: string
slotsById: Map<string, SlotDto>
onGoToSlot: (slot: SlotDto) => void
}) {
}>) {
const { t } = useTranslation()
const { data: issues } = useQuery({
queryKey: qk.channels.issues(channelId),
@@ -53,11 +53,11 @@ function IssueRow({
issue,
slot,
onGoToSlot,
}: {
}: Readonly<{
issue: TemplateIssueDto
slot: SlotDto | undefined
onGoToSlot: (slot: SlotDto) => void
}) {
}>) {
const { t } = useTranslation()
const Icon = issue.severity === 'Error' ? CircleAlert : AlertTriangle
@@ -27,7 +27,7 @@ const PROGRAMME_KINDS = new Set<PlannedItemKind>(['Program', 'Fallback', 'SignOf
* Предпросмотр (см. 6.4): прогон генератора по текущим правилам без записи и без продвижения
* курсоров. Заставки приходят резервом известной длины — реальный рендер только при применении.
*/
export function TemplatePreview({ channelId }: { channelId: string }) {
export function TemplatePreview({ channelId }: Readonly<{ channelId: string }>) {
const { t } = useTranslation()
const [open, setOpen] = useState(false)
const [days, setDays] = useState(1)
@@ -97,7 +97,7 @@ export function TemplatePreview({ channelId }: { channelId: string }) {
)
}
function Programme({ preview }: { preview: SchedulePreviewDto }) {
function Programme({ preview }: Readonly<{ preview: SchedulePreviewDto }>) {
const { t } = useTranslation()
const items = preview.items.filter((i) => PROGRAMME_KINDS.has(i.kind))
@@ -143,7 +143,7 @@ function loadByHour(preview: SchedulePreviewDto): { hour: Date; minutes: number
.map(([hour, minutes]) => ({ hour: new Date(hour), minutes }))
}
function Tape({ preview }: { preview: SchedulePreviewDto }) {
function Tape({ preview }: Readonly<{ preview: SchedulePreviewDto }>) {
const { t } = useTranslation()
const load = useMemo(() => loadByHour(preview), [preview])
const peak = Math.max(1, ...load.map((l) => l.minutes))
@@ -180,7 +180,10 @@ function Tape({ preview }: { preview: SchedulePreviewDto }) {
)
}
function TapeRow({ item, preview }: { item: PreviewItemDto; preview: SchedulePreviewDto }) {
function TapeRow({
item,
preview,
}: Readonly<{ item: PreviewItemDto; preview: SchedulePreviewDto }>) {
const { t } = useTranslation()
const minutes =
(new Date(item.endsAtUtc).getTime() - new Date(item.startsAtUtc).getTime()) / 60_000
@@ -200,7 +203,7 @@ function TapeRow({ item, preview }: { item: PreviewItemDto; preview: SchedulePre
}
/** Предупреждения, сгруппированные по виду: десять однотипных строк читаются как одна проблема. */
function Problems({ preview }: { preview: SchedulePreviewDto }) {
function Problems({ preview }: Readonly<{ preview: SchedulePreviewDto }>) {
const { t } = useTranslation()
// Ключ строки — «вид + порядковый номер внутри вида»: у предупреждения нет своего id, а текст
// повторяется (одна и та же причина на разных слотах), и позиция здесь — единственное, что его
@@ -250,7 +253,7 @@ function Problems({ preview }: { preview: SchedulePreviewDto }) {
* Тепловая карта повторов: матрица «шоу × вещательные сутки», яркость — число показов. Сразу видно,
* что один фильм крутится четыре раза за неделю.
*/
function RepeatHeatmap({ preview }: { preview: SchedulePreviewDto }) {
function RepeatHeatmap({ preview }: Readonly<{ preview: SchedulePreviewDto }>) {
const { t } = useTranslation()
const { days, rows } = useMemo(() => {
@@ -21,12 +21,12 @@ export function ViewerCard({
bare,
onSaved,
onError,
}: {
}: Readonly<{
channel: ChannelDto
bare?: boolean
onSaved: () => void
onError: (error: unknown) => void
}) {
}>) {
const { t } = useTranslation()
const [viewer, setViewer] = useState<ViewerSettings>(channel.viewer)
const [galleryOpen, setGalleryOpen] = useState(false)
@@ -22,7 +22,7 @@ import {
updateCollection,
} from './api'
export function CollectionDetail({ collectionId }: { collectionId: string }) {
export function CollectionDetail({ collectionId }: Readonly<{ collectionId: string }>) {
const { t } = useTranslation()
const queryClient = useQueryClient()
const [galleryOpen, setGalleryOpen] = useState(false)
@@ -73,12 +73,12 @@ export function GenresPanel() {
mutationFn: ({
id,
...body
}: {
}: Readonly<{
id: string
name: string
sortOrder: number
aliases: string[]
}) => updateGenre(id, body),
}>) => updateGenre(id, body),
onSuccess: invalidate,
})
const deleteMutation = useMutation({
@@ -2,7 +2,7 @@ import { useTranslation } from 'react-i18next'
import { splitDuration } from './format'
/** Объём эфира: «118 ч 40 мин». Ноль — прочерк, потому что «0 ч» читается как сбой подсчёта. */
export function DurationLabel({ seconds }: { seconds: number }) {
export function DurationLabel({ seconds }: Readonly<{ seconds: number }>) {
const { t } = useTranslation()
const parts = splitDuration(seconds)
if (!parts) return <></>
@@ -25,7 +25,7 @@ import { GroupFilterPanel } from './GroupFilterPanel'
const EMPTY_FILTER: GroupFilter = {}
export function GroupDetail({ groupId }: { groupId: string }) {
export function GroupDetail({ groupId }: Readonly<{ groupId: string }>) {
const { t } = useTranslation()
const queryClient = useQueryClient()
@@ -88,7 +88,7 @@ export function GroupDetail({ groupId }: { groupId: string }) {
onError,
})
const weightMutation = useMutation({
mutationFn: ({ itemId, weight }: { itemId: string; weight: number }) =>
mutationFn: ({ itemId, weight }: Readonly<{ itemId: string; weight: number }>) =>
setGroupItemWeight(groupId, itemId, weight),
onSuccess: invalidate,
onError,
@@ -14,10 +14,10 @@ const SHOW_KINDS: ShowKind[] = ['Series', 'Single']
export function GroupFilterPanel({
filter,
onChange,
}: {
}: Readonly<{
filter: GroupFilter
onChange: (next: GroupFilter) => void
}) {
}>) {
const { t } = useTranslation()
const { data: genres } = useQuery({ queryKey: qk.genres.all, queryFn: listGenres })
@@ -25,11 +25,11 @@ export function GalleryBrowser({
category = 'Library',
onSelect,
onClose,
}: {
}: Readonly<{
category?: ImageCategory
onSelect?: (image: ImagePick) => void
onClose?: () => void
}) {
}>) {
const { t } = useTranslation()
const queryClient = useQueryClient()
const [active, setActive] = useState<ImageCategory>(category)
@@ -193,12 +193,12 @@ export function ImageGallery({
onOpenChange,
category,
onSelect,
}: {
}: Readonly<{
open: boolean
onOpenChange: (open: boolean) => void
category?: ImageCategory
onSelect?: (image: ImagePick) => void
}) {
}>) {
const { t } = useTranslation()
return (
<Dialog open={open} onOpenChange={onOpenChange}>
@@ -16,10 +16,10 @@ import { formatClock } from './format'
export function BlockBuilder({
onSaved,
onError,
}: {
}: Readonly<{
onSaved: () => void
onError: (error: unknown) => void
}) {
}>) {
const { t } = useTranslation()
const [name, setName] = useState('')
const [items, setItems] = useState<ClipDragItem[]>([])
@@ -15,7 +15,7 @@ import { formatClock } from './format'
* Группы роликов собираются здесь же, а не в общем редакторе групп (см. 6.7): в выбранную группу
* перетаскиваются и отдельные ролики, и готовые блоки — стык умеет и то и другое.
*/
export function ClipGroupPanel({ onError }: { onError: (error: unknown) => void }) {
export function ClipGroupPanel({ onError }: Readonly<{ onError: (error: unknown) => void }>) {
const { t } = useTranslation()
const queryClient = useQueryClient()
const [selected, setSelected] = useState('')
@@ -48,7 +48,7 @@ export function InterstitialsPanel() {
const onError = useApiError()
const renameMutation = useMutation({
mutationFn: ({ id, name }: { id: string; name: string }) => renameShow(id, name),
mutationFn: ({ id, name }: Readonly<{ id: string; name: string }>) => renameShow(id, name),
onSuccess: () => {
setRenaming(null)
invalidate()
@@ -45,7 +45,7 @@ function formatSize(bytes: number): string {
* Номера сезона и серии распознаются здесь же и в таком виде и уходят на сервер: что показано,
* то и сохранится.
*/
export function ManualInboxDialog({ onClose }: { onClose: () => void }) {
export function ManualInboxDialog({ onClose }: Readonly<{ onClose: () => void }>) {
const { t } = useTranslation()
const queryClient = useQueryClient()
const [selected, setSelected] = useState<string[]>([])
@@ -261,7 +261,7 @@ export function MediaPanel() {
)
}
function MediaRow({ asset, onDelete }: { asset: MediaAssetDto; onDelete: () => void }) {
function MediaRow({ asset, onDelete }: Readonly<{ asset: MediaAssetDto; onDelete: () => void }>) {
const { t } = useTranslation()
return (
<tr className="border-b border-border last:border-0">
@@ -3,7 +3,7 @@ import { AlertCircle, Check, ChevronDown, ChevronUp, Clock, Loader2, RotateCw, X
import { cn } from '@/shared/lib/cn'
import { type UploadItem, useUploadStore } from './upload-store'
function StatusIcon({ status }: { status: UploadItem['status'] }) {
function StatusIcon({ status }: Readonly<{ status: UploadItem['status'] }>) {
switch (status) {
case 'done':
return <Check className="h-3.5 w-3.5 shrink-0 text-primary" />
@@ -23,7 +23,10 @@ import { useUploadStore } from './upload-store'
/** Radix Select запрещает пустое значение — под «в библиотеку» используем спец-значение. */
const LIBRARY_VALUE = '__library__'
export function UploadToShowDialog({ files, onClose }: { files: File[]; onClose: () => void }) {
export function UploadToShowDialog({
files,
onClose,
}: Readonly<{ files: File[]; onClose: () => void }>) {
const { t } = useTranslation()
const enqueue = useUploadStore((s) => s.enqueue)
const [seasonStr, setSeasonStr] = useState('')
@@ -50,7 +50,7 @@ export function RolesPanel() {
})
const renameMutation = useMutation({
mutationFn: ({ id, name }: { id: string; name: string }) => updateRole(id, name),
mutationFn: ({ id, name }: Readonly<{ id: string; name: string }>) => updateRole(id, name),
onSuccess: invalidate,
onError,
})
@@ -44,7 +44,7 @@ function addButtonLabel(
return `${t('admin.shows.addSelected')} (${count})`
}
export function ShowDetail({ showId }: { showId: string }) {
export function ShowDetail({ showId }: Readonly<{ showId: string }>) {
const { t } = useTranslation()
const queryClient = useQueryClient()
const [filter, setFilter] = useState('')
@@ -15,7 +15,10 @@ import { setShowGenres } from './api'
* Жанры шоу: бейджи в шапке карточки + диалог правки. Основной жанр отмечается отдельно —
* он показывается в списке шоу и участвует в отборе контента наравне с остальными.
*/
export function ShowGenresField({ show, onChanged }: { show: ShowDto; onChanged: () => void }) {
export function ShowGenresField({
show,
onChanged,
}: Readonly<{ show: ShowDto; onChanged: () => void }>) {
const { t } = useTranslation()
const [open, setOpen] = useState(false)
const [selected, setSelected] = useState<string[]>([])
@@ -28,7 +28,10 @@ import {
updateMetadata,
} from './api'
export function ShowMetadataCard({ show, onChanged }: { show: ShowDto; onChanged: () => void }) {
export function ShowMetadataCard({
show,
onChanged,
}: Readonly<{ show: ShowDto; onChanged: () => void }>) {
const { t } = useTranslation()
const [galleryOpen, setGalleryOpen] = useState(false)
const [provider, setProvider] = useState('')
@@ -359,7 +362,7 @@ export function ShowMetadataCard({ show, onChanged }: { show: ShowDto; onChanged
}
/** Итог по сезону: чего не хватает — или что полный состав сезона неизвестен. */
function SeasonGapNote({ gap }: { gap: MissingEpisodesReport['seasons'][number] }) {
function SeasonGapNote({ gap }: Readonly<{ gap: MissingEpisodesReport['seasons'][number] }>) {
const { t } = useTranslation()
if (gap.expected == null)
@@ -64,7 +64,7 @@ export function UsersPanel() {
const unblockMutation = useMutation({ mutationFn: unblockUser, onSuccess: invalidate, onError })
const deleteMutation = useMutation({ mutationFn: deleteUser, onSuccess: invalidate, onError })
const changeRoleMutation = useMutation({
mutationFn: ({ userId, roleId: nextRoleId }: { userId: string; roleId: string }) =>
mutationFn: ({ userId, roleId: nextRoleId }: Readonly<{ userId: string; roleId: string }>) =>
changeUserRole(userId, nextRoleId),
onSuccess: invalidate,
onError,
@@ -302,11 +302,11 @@ function ResetPasswordDialog({
user,
onClose,
onError,
}: {
}: Readonly<{
user: UserSummaryDto
onClose: () => void
onError: (e: unknown) => void
}) {
}>) {
const { t } = useTranslation()
const [password, setPassword] = useState('')