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.
This commit is contained in:
@@ -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('')
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ const schema = z.object({
|
||||
|
||||
type FormValues = z.infer<typeof schema>
|
||||
|
||||
export function LoginForm({ onSuccess }: { onSuccess: () => void }) {
|
||||
export function LoginForm({ onSuccess }: Readonly<{ onSuccess: () => void }>) {
|
||||
const { t } = useTranslation()
|
||||
const {
|
||||
register: registerField,
|
||||
|
||||
@@ -16,7 +16,7 @@ const schema = z.object({
|
||||
|
||||
type FormValues = z.infer<typeof schema>
|
||||
|
||||
export function RegisterForm({ onSuccess }: { onSuccess: () => void }) {
|
||||
export function RegisterForm({ onSuccess }: Readonly<{ onSuccess: () => void }>) {
|
||||
const { t } = useTranslation()
|
||||
const {
|
||||
register: registerField,
|
||||
|
||||
@@ -15,7 +15,7 @@ function formatTime(iso: string) {
|
||||
}
|
||||
|
||||
/** Кадр серии, если он есть; иначе постер шоу (он вертикальный, отсюда другая ширина). */
|
||||
function EntryThumb({ entry }: { entry: PublicEpgEntryDto | undefined }) {
|
||||
function EntryThumb({ entry }: Readonly<{ entry: PublicEpgEntryDto | undefined }>) {
|
||||
if (entry?.episodeStillImageId)
|
||||
return (
|
||||
<img
|
||||
|
||||
@@ -58,7 +58,7 @@ export function ChannelPlayer({
|
||||
nextUp,
|
||||
flash,
|
||||
onUnavailable,
|
||||
}: {
|
||||
}: Readonly<{
|
||||
slug: string
|
||||
/** Канал, чьи оверлеи рисуем. Всё опционально: канал без логотипа и без шума — норма. */
|
||||
channel?: PublicChannelDto
|
||||
@@ -67,7 +67,7 @@ export function ChannelPlayer({
|
||||
/** Показать чёрный кадр с номером — переключение по номерам, как на телевизоре. */
|
||||
flash?: boolean
|
||||
onUnavailable?: () => void
|
||||
}) {
|
||||
}>) {
|
||||
const { t } = useTranslation()
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const videoRef = useRef<HTMLVideoElement>(null)
|
||||
|
||||
@@ -19,11 +19,11 @@ export function ChannelLogo({
|
||||
imageId,
|
||||
corner,
|
||||
opacity,
|
||||
}: {
|
||||
}: Readonly<{
|
||||
imageId: string
|
||||
corner: LogoCorner
|
||||
opacity: number
|
||||
}) {
|
||||
}>) {
|
||||
return (
|
||||
<img
|
||||
src={imageUrl(imageId)}
|
||||
@@ -52,7 +52,7 @@ export function ScreenClock() {
|
||||
}
|
||||
|
||||
/** Плашка «Далее: …» — данные уже есть в EPG, отдельного запроса не нужно. */
|
||||
export function NextUpBanner({ title }: { title: string }) {
|
||||
export function NextUpBanner({ title }: Readonly<{ title: string }>) {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<span className="pointer-events-none absolute bottom-14 left-3 rounded bg-black/60 px-2 py-1 text-sm text-white">
|
||||
@@ -65,7 +65,7 @@ export function NextUpBanner({ title }: { title: string }) {
|
||||
* Аналоговый фильтр: лёгкий VHS-шум, дрожание и размытие краёв. Переборщить очень легко, поэтому
|
||||
* сила регулируется, а вклад каждого слоя от неё убывает нелинейно.
|
||||
*/
|
||||
export function AnalogFilter({ strength }: { strength: number }) {
|
||||
export function AnalogFilter({ strength }: Readonly<{ strength: number }>) {
|
||||
const s = Math.min(1, Math.max(0, strength))
|
||||
return (
|
||||
<>
|
||||
@@ -91,7 +91,7 @@ export function AnalogFilter({ strength }: { strength: number }) {
|
||||
}
|
||||
|
||||
/** Короткий чёрный кадр с номером канала — как при переключении на телевизоре. */
|
||||
export function ChannelFlash({ number, name }: { number: number | null; name: string }) {
|
||||
export function ChannelFlash({ number, name }: Readonly<{ number: number | null; name: string }>) {
|
||||
return (
|
||||
<div className="pointer-events-none absolute inset-0 flex items-start justify-end bg-black">
|
||||
<span className="m-6 flex items-baseline gap-2 text-white">
|
||||
|
||||
Reference in New Issue
Block a user