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
@@ -18,10 +18,10 @@ public class Result
public static Result Success() => new(true, Error.None); public static Result Success() => new(true, Error.None);
public static Result Failure(Error error) => new(false, error);
public static Result<T> Success<T>(T value) => new(value, true, Error.None); public static Result<T> Success<T>(T value) => new(value, true, Error.None);
public static Result Failure(Error error) => new(false, error);
public static Result<T> Failure<T>(Error error) => new(default, false, error); public static Result<T> Failure<T>(Error error) => new(default, false, error);
} }
@@ -195,7 +195,7 @@ public sealed class GroupExpander(IAppDbContext dbContext)
e.ChannelId == channelId e.ChannelId == channelId
&& e.Kind == ScheduleEntryKind.Program && e.Kind == ScheduleEntryKind.Program
&& e.ShowId != null && e.ShowId != null
&& showIds.Contains(e.ShowId!.Value) && showIds.Contains(e.ShowId.Value)
) )
.GroupBy(e => e.ShowId!.Value) .GroupBy(e => e.ShowId!.Value)
.Select(g => new { ShowId = g.Key, LastPlayed = g.Max(e => e.StartsAtUtc) }) .Select(g => new { ShowId = g.Key, LastPlayed = g.Max(e => e.StartsAtUtc) })
@@ -221,7 +221,7 @@ public sealed class GroupExpander(IAppDbContext dbContext)
e.ChannelId == channelId e.ChannelId == channelId
&& e.Kind == ScheduleEntryKind.Program && e.Kind == ScheduleEntryKind.Program
&& e.ShowId != null && e.ShowId != null
&& showIds.Contains(e.ShowId!.Value) && showIds.Contains(e.ShowId.Value)
&& e.StartsAtUtc >= since && e.StartsAtUtc >= since
) )
.Select(e => new { ShowId = e.ShowId!.Value, e.StartsAtUtc }) .Select(e => new { ShowId = e.ShowId!.Value, e.StartsAtUtc })
@@ -50,12 +50,8 @@ public sealed record PlanningRules(
// Пересекающиеся окна разрешаются в пользу строгого: детское время не должно // Пересекающиеся окна разрешаются в пользу строгого: детское время не должно
// отменяться более широким окном, случайно наложенным сверху. // отменяться более широким окном, случайно наложенным сверху.
ShowAudience? strictest = null; var applicable = windows.Where(w => w.Contains(moment)).Select(w => w.MaxAudience).ToList();
foreach (var window in windows.Where(w => w.Contains(moment))) return applicable.Count > 0 ? applicable.Min() : null;
strictest = strictest is { } current && current <= window.MaxAudience
? current
: window.MaxAudience;
return strictest;
} }
public string ToJson() => JsonSerializer.Serialize(this, Options); public string ToJson() => JsonSerializer.Serialize(this, Options);
@@ -110,7 +110,7 @@ internal sealed class RoleService(
if (currentRoles.Count > 0) if (currentRoles.Count > 0)
await userManager.RemoveFromRolesAsync(user, currentRoles); await userManager.RemoveFromRolesAsync(user, currentRoles);
await userManager.AddToRoleAsync(user, role.Name!); await userManager.AddToRoleAsync(user, role.Name);
return Result.Success(); return Result.Success();
} }
@@ -31,7 +31,7 @@ import { ViewerCard } from './components/ViewerCard'
const TABS = ['settings', 'grid', 'rules', 'junctions', 'bumpers', 'viewer', 'air'] as const const TABS = ['settings', 'grid', 'rules', 'junctions', 'bumpers', 'viewer', 'air'] as const
type ChannelTab = (typeof TABS)[number] type ChannelTab = (typeof TABS)[number]
export function ChannelDetail({ channelId }: { channelId: string }) { export function ChannelDetail({ channelId }: Readonly<{ channelId: string }>) {
const { t } = useTranslation() const { t } = useTranslation()
const queryClient = useQueryClient() const queryClient = useQueryClient()
const [applyOpen, setApplyOpen] = useState(false) const [applyOpen, setApplyOpen] = useState(false)
@@ -52,7 +52,7 @@ export function ChannelDetail({ channelId }: { channelId: string }) {
}) })
const { data: schedule } = useQuery({ const { data: schedule } = useQuery({
queryKey: qk.channels.schedule(channelId), 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 = () => { const invalidate = () => {
@@ -25,13 +25,13 @@ export function ApplyDialog({
pending, pending,
onApply, onApply,
onClose, onClose,
}: { }: Readonly<{
channelId: string channelId: string
utcOffsetMinutes: number utcOffsetMinutes: number
pending: boolean pending: boolean
onApply: () => void onApply: () => void
onClose: () => void onClose: () => void
}) { }>) {
const { t } = useTranslation() const { t } = useTranslation()
const { data, isFetching } = useQuery({ const { data, isFetching } = useQuery({
queryKey: qk.channels.diff(channelId), queryKey: qk.channels.diff(channelId),
@@ -13,13 +13,13 @@ export function BumperBackgroundField({
backgroundImageId, backgroundImageId,
onChanged, onChanged,
onError, onError,
}: { }: Readonly<{
channelId: string channelId: string
templateId: string templateId: string
backgroundImageId: string | null backgroundImageId: string | null
onChanged: () => void onChanged: () => void
onError: (e: unknown) => void onError: (e: unknown) => void
}) { }>) {
const { t } = useTranslation() const { t } = useTranslation()
const [galleryOpen, setGalleryOpen] = useState(false) const [galleryOpen, setGalleryOpen] = useState(false)
@@ -15,12 +15,12 @@ export function BumperCard({
bare, bare,
onSaved, onSaved,
onError, onError,
}: { }: Readonly<{
channel: ChannelDto channel: ChannelDto
bare?: boolean bare?: boolean
onSaved: () => void onSaved: () => void
onError: (e: unknown) => void onError: (e: unknown) => void
}) { }>) {
const { t } = useTranslation() const { t } = useTranslation()
const [bumpersEnabled, setBumpersEnabled] = useState(channel.bumpersEnabled) const [bumpersEnabled, setBumpersEnabled] = useState(channel.bumpersEnabled)
const [bumper, setBumper] = useState<BumperSettings>(channel.bumper) const [bumper, setBumper] = useState<BumperSettings>(channel.bumper)
@@ -15,7 +15,7 @@ export function BumperFileUpload({
clear, clear,
onSaved, onSaved,
onError, onError,
}: { }: Readonly<{
channelId: string channelId: string
templateId: string templateId: string
kind: string kind: string
@@ -27,7 +27,7 @@ export function BumperFileUpload({
clear: (id: string, templateId: string) => Promise<void> clear: (id: string, templateId: string) => Promise<void>
onSaved: () => void onSaved: () => void
onError: (e: unknown) => void onError: (e: unknown) => void
}) { }>) {
const { t } = useTranslation() const { t } = useTranslation()
const inputId = `bumper-${kind}-${templateId}` const inputId = `bumper-${kind}-${templateId}`
@@ -11,12 +11,12 @@ export function BumperPreviewPlayer({
templateId, templateId,
variants, variants,
onError, onError,
}: { }: Readonly<{
channelId: string channelId: string
templateId: string templateId: string
variants: BumperTextVariantDto[] variants: BumperTextVariantDto[]
onError: (e: unknown) => void onError: (e: unknown) => void
}) { }>) {
const { t } = useTranslation() const { t } = useTranslation()
const [ready, setReady] = useState(false) const [ready, setReady] = useState(false)
const [bust, setBust] = useState(0) const [bust, setBust] = useState(0)
@@ -26,12 +26,12 @@ export function BumperTemplateEditor({
template, template,
onChanged, onChanged,
onError, onError,
}: { }: Readonly<{
channelId: string channelId: string
template: BumperTemplateDto template: BumperTemplateDto
onChanged: () => void onChanged: () => void
onError: (e: unknown) => void onError: (e: unknown) => void
}) { }>) {
const { t } = useTranslation() const { t } = useTranslation()
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false)
const [name, setName] = useState(template.name) const [name, setName] = useState(template.name)
@@ -16,14 +16,14 @@ export function BumperVariantEditor({
canRemove, canRemove,
onChanged, onChanged,
onError, onError,
}: { }: Readonly<{
channelId: string channelId: string
templateId: string templateId: string
variant: BumperTextVariantDto variant: BumperTextVariantDto
canRemove: boolean canRemove: boolean
onChanged: () => void onChanged: () => void
onError: (e: unknown) => void onError: (e: unknown) => void
}) { }>) {
const { t } = useTranslation() const { t } = useTranslation()
const [form, setForm] = useState({ const [form, setForm] = useState({
name: variant.name, name: variant.name,
@@ -9,14 +9,14 @@ export function CollapsibleCard({
bare = false, bare = false,
contentClassName, contentClassName,
children, children,
}: { }: Readonly<{
title: string title: string
defaultOpen?: boolean defaultOpen?: boolean
/** Без своего заголовка и сворачивания — когда карточка и так лежит во вкладке с этим названием. */ /** Без своего заголовка и сворачивания — когда карточка и так лежит во вкладке с этим названием. */
bare?: boolean bare?: boolean
contentClassName?: string contentClassName?: string
children: ReactNode children: ReactNode
}) { }>) {
const [open, setOpen] = useState(defaultOpen) const [open, setOpen] = useState(defaultOpen)
if (bare) if (bare)
@@ -19,11 +19,11 @@ export function EntryTraceDialog({
entryId, entryId,
utcOffsetMinutes, utcOffsetMinutes,
onClose, onClose,
}: { }: Readonly<{
entryId: string entryId: string
utcOffsetMinutes: number utcOffsetMinutes: number
onClose: () => void onClose: () => void
}) { }>) {
const { t } = useTranslation() const { t } = useTranslation()
const { data } = useQuery({ const { data } = useQuery({
queryKey: qk.entries.trace(entryId), 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 ( return (
<> <>
<dt className="text-muted-foreground">{label}</dt> <dt className="text-muted-foreground">{label}</dt>
@@ -38,13 +38,13 @@ export function GridTab({
templateError, templateError,
onChanged, onChanged,
onError, onError,
}: { }: Readonly<{
channelId: string channelId: string
template: ScheduleTemplateDto | undefined template: ScheduleTemplateDto | undefined
templateError: unknown templateError: unknown
onChanged: () => void onChanged: () => void
onError: (error: unknown) => void onError: (error: unknown) => void
}) { }>) {
const { t } = useTranslation() const { t } = useTranslation()
const [draft, setDraft] = useState<SlotDraft | null>(null) const [draft, setDraft] = useState<SlotDraft | null>(null)
const [activeLayerId, setActiveLayerId] = useState<string | null>(null) const [activeLayerId, setActiveLayerId] = useState<string | null>(null)
@@ -141,17 +141,17 @@ export function GridTab({
slot, slot,
weekday, weekday,
startMinutes, startMinutes,
}: { }: Readonly<{
slot: SlotDto slot: SlotDto
weekday: number weekday: number
startMinutes: number startMinutes: number
}) => updateSlot(slot.id, { ...toSlotBody(slot), weekday, targetStart: toTime(startMinutes) }), }>) => updateSlot(slot.id, { ...toSlotBody(slot), weekday, targetStart: toTime(startMinutes) }),
onSuccess: onChanged, onSuccess: onChanged,
onError, onError,
}) })
const resizeSlotMutation = useMutation({ 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 }), updateSlot(slot.id, { ...toSlotBody(slot), targetDurationMinutes: minutes }),
onSuccess: onChanged, onSuccess: onChanged,
onError, onError,
@@ -159,7 +159,7 @@ export function GridTab({
/** Копирование дня: слоты «каждый день» не копируются — они и так есть во всех колонках. */ /** Копирование дня: слоты «каждый день» не копируются — они и так есть во всех колонках. */
const copyDayMutation = useMutation({ 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) => const sources = (template?.layers ?? []).flatMap((layer) =>
layer.slots.filter((slot) => slot.weekday === from).map((slot) => ({ layer, slot })), layer.slots.filter((slot) => slot.weekday === from).map((slot) => ({ layer, slot })),
) )
@@ -44,14 +44,14 @@ export function JunctionElementDialog({
onClose, onClose,
onChanged, onChanged,
onError, onError,
}: { }: Readonly<{
junctionId: string junctionId: string
element: JunctionElementDto element: JunctionElementDto
bumperTemplates: BumperTemplateDto[] bumperTemplates: BumperTemplateDto[]
onClose: () => void onClose: () => void
onChanged: () => void onChanged: () => void
onError: (error: unknown) => void onError: (error: unknown) => void
}) { }>) {
const { t } = useTranslation() const { t } = useTranslation()
const [body, setBody] = useState<JunctionElementBody>(() => toBody(element)) const [body, setBody] = useState<JunctionElementBody>(() => toBody(element))
const { data: groups } = useQuery({ queryKey: qk.groups.all, queryFn: listGroups }) const { data: groups } = useQuery({ queryKey: qk.groups.all, queryFn: listGroups })
@@ -86,13 +86,13 @@ export function JunctionsCard({
bare, bare,
onChanged, onChanged,
onError, onError,
}: { }: Readonly<{
channel: ChannelDto channel: ChannelDto
template: ScheduleTemplateDto | undefined template: ScheduleTemplateDto | undefined
bare?: boolean bare?: boolean
onChanged: () => void onChanged: () => void
onError: (error: unknown) => void onError: (error: unknown) => void
}) { }>) {
const { t } = useTranslation() const { t } = useTranslation()
const [newName, setNewName] = useState('') const [newName, setNewName] = useState('')
@@ -186,13 +186,13 @@ function JunctionChain({
groups, groups,
onChanged, onChanged,
onError, onError,
}: { }: Readonly<{
junction: JunctionTemplateDto junction: JunctionTemplateDto
channel: ChannelDto channel: ChannelDto
groups: GroupSummaryDto[] | undefined groups: GroupSummaryDto[] | undefined
onChanged: () => void onChanged: () => void
onError: (error: unknown) => void onError: (error: unknown) => void
}) { }>) {
const { t } = useTranslation() const { t } = useTranslation()
const [name, setName] = useState<string | null>(null) const [name, setName] = useState<string | null>(null)
const [dragged, setDragged] = useState<string | null>(null) const [dragged, setDragged] = useState<string | null>(null)
@@ -29,12 +29,12 @@ export function LayerApplicabilityDialog({
onClose, onClose,
onChanged, onChanged,
onError, onError,
}: { }: Readonly<{
layer: GridLayerDto layer: GridLayerDto
onClose: () => void onClose: () => void
onChanged: () => void onChanged: () => void
onError: (error: unknown) => void onError: (error: unknown) => void
}) { }>) {
const { t } = useTranslation() const { t } = useTranslation()
const [name, setName] = useState(layer.name) const [name, setName] = useState(layer.name)
const [weekdays, setWeekdays] = useState<number[]>(layer.applicability?.weekdays ?? []) const [weekdays, setWeekdays] = useState<number[]>(layer.applicability?.weekdays ?? [])
@@ -199,12 +199,12 @@ function Section({
onAdd, onAdd,
empty, empty,
children, children,
}: { }: Readonly<{
title: string title: string
onAdd: () => void onAdd: () => void
empty: boolean empty: boolean
children: React.ReactNode children: React.ReactNode
}) { }>) {
const { t } = useTranslation() const { t } = useTranslation()
return ( return (
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
@@ -228,11 +228,11 @@ function MonthDay({
value, value,
prefix, prefix,
onChange, onChange,
}: { }: Readonly<{
value: AnnualRange value: AnnualRange
prefix: 'from' | 'to' prefix: 'from' | 'to'
onChange: (part: Partial<AnnualRange>) => void onChange: (part: Partial<AnnualRange>) => void
}) { }>) {
const month = prefix === 'from' ? value.fromMonth : value.toMonth const month = prefix === 'from' ? value.fromMonth : value.toMonth
const day = prefix === 'from' ? value.fromDay : value.toDay const day = prefix === 'from' ? value.fromDay : value.toDay
@@ -37,12 +37,12 @@ export function RulesCard({
bare, bare,
onChanged, onChanged,
onError, onError,
}: { }: Readonly<{
template: ScheduleTemplateDto template: ScheduleTemplateDto
bare?: boolean bare?: boolean
onChanged: () => void onChanged: () => void
onError: (error: unknown) => void onError: (error: unknown) => void
}) { }>) {
const { t } = useTranslation() const { t } = useTranslation()
const [windows, setWindows] = useState<WindowRow[]>(() => const [windows, setWindows] = useState<WindowRow[]>(() =>
toRows(template.rules?.maxAudienceByTime ?? []), toRows(template.rules?.maxAudienceByTime ?? []),
@@ -56,7 +56,7 @@ export function ScheduleGrid({
onMoveSlot, onMoveSlot,
onResizeSlot, onResizeSlot,
onCopyDay, onCopyDay,
}: { }: Readonly<{
template: ScheduleTemplateDto template: ScheduleTemplateDto
selectedSlotId: string | null selectedSlotId: string | null
/** Дата, на которую смотрим сетку («показать 25 декабря»); null — все слои разом. */ /** Дата, на которую смотрим сетку («показать 25 декабря»); null — все слои разом. */
@@ -67,7 +67,7 @@ export function ScheduleGrid({
onMoveSlot: (slot: SlotDto, weekday: number, startMinutes: number) => void onMoveSlot: (slot: SlotDto, weekday: number, startMinutes: number) => void
onResizeSlot: (slot: SlotDto, durationMinutes: number) => void onResizeSlot: (slot: SlotDto, durationMinutes: number) => void
onCopyDay: (fromWeekday: number) => void onCopyDay: (fromWeekday: number) => void
}) { }>) {
const { t } = useTranslation() const { t } = useTranslation()
const dayStart = template.dayStartTime.slice(0, 5) const dayStart = template.dayStartTime.slice(0, 5)
const dayStartMinutes = minutesOf(dayStart) const dayStartMinutes = minutesOf(dayStart)
@@ -280,7 +280,7 @@ export function LayerList({
onToggle, onToggle,
onReorder, onReorder,
onEditApplicability, onEditApplicability,
}: { }: Readonly<{
template: ScheduleTemplateDto template: ScheduleTemplateDto
activeLayerId: string | null activeLayerId: string | null
viewDate: string | null viewDate: string | null
@@ -289,7 +289,7 @@ export function LayerList({
onToggle: (layer: GridLayerDto) => void onToggle: (layer: GridLayerDto) => void
onReorder: (layerIdsTopFirst: string[]) => void onReorder: (layerIdsTopFirst: string[]) => void
onEditApplicability: (layer: GridLayerDto) => void onEditApplicability: (layer: GridLayerDto) => void
}) { }>) {
const { t } = useTranslation() const { t } = useTranslation()
const [dragged, setDragged] = useState<string | null>(null) const [dragged, setDragged] = useState<string | null>(null)
@@ -5,7 +5,7 @@ import { Badge } from '@/shared/ui/badge'
import { formatTime } from '../lib/format' import { formatTime } from '../lib/format'
/** Что стоит в строке расписания: реклама, заставка-переход или программа с номером серии. */ /** Что стоит в строке расписания: реклама, заставка-переход или программа с номером серии. */
function EntryLabel({ entry }: { entry: ScheduleEntryDto }) { function EntryLabel({ entry }: Readonly<{ entry: ScheduleEntryDto }>) {
const { t } = useTranslation() const { t } = useTranslation()
if (entry.kind === 'Ad') return <Badge variant="muted">{t('air.ad')}</Badge> if (entry.kind === 'Ad') return <Badge variant="muted">{t('air.ad')}</Badge>
@@ -35,7 +35,7 @@ function EntryLabel({ entry }: { entry: ScheduleEntryDto }) {
} }
/** «· S02E05» либо «· серия N» — что удалось распознать; ничего, если ни того ни другого нет. */ /** «· S02E05» либо «· серия N» — что удалось распознать; ничего, если ни того ни другого нет. */
function EpisodeSuffix({ entry }: { entry: ScheduleEntryDto }) { function EpisodeSuffix({ entry }: Readonly<{ entry: ScheduleEntryDto }>) {
const { t } = useTranslation() const { t } = useTranslation()
if (entry.seasonEpisode) if (entry.seasonEpisode)
@@ -54,10 +54,10 @@ function EpisodeSuffix({ entry }: { entry: ScheduleEntryDto }) {
export function SchedulePreview({ export function SchedulePreview({
entries, entries,
onShowTrace, onShowTrace,
}: { }: Readonly<{
entries: ScheduleEntryDto[] entries: ScheduleEntryDto[]
onShowTrace: (entryId: string) => void onShowTrace: (entryId: string) => void
}) { }>) {
const { t } = useTranslation() const { t } = useTranslation()
if (entries.length === 0) if (entries.length === 0)
return <p className="text-muted-foreground">{t('admin.channels.noSchedule')}</p> return <p className="text-muted-foreground">{t('admin.channels.noSchedule')}</p>
@@ -16,13 +16,13 @@ export function SettingsCard({
bare, bare,
onSaved, onSaved,
onError, onError,
}: { }: Readonly<{
channel: ChannelDto channel: ChannelDto
readyAssets: { id: string; originalFileName: string }[] readyAssets: { id: string; originalFileName: string }[]
bare?: boolean bare?: boolean
onSaved: () => void onSaved: () => void
onError: (e: unknown) => void onError: (e: unknown) => void
}) { }>) {
const { t } = useTranslation() const { t } = useTranslation()
const [name, setName] = useState(channel.name) const [name, setName] = useState(channel.name)
const [isEnabled, setIsEnabled] = useState(channel.isEnabled) const [isEnabled, setIsEnabled] = useState(channel.isEnabled)
@@ -68,12 +68,12 @@ export function SlotInspector({
draft, draft,
onClose, onClose,
onChanged, onChanged,
}: { }: Readonly<{
channelId: string channelId: string
draft: SlotDraft draft: SlotDraft
onClose: () => void onClose: () => void
onChanged: () => void onChanged: () => void
}) { }>) {
const { t } = useTranslation() const { t } = useTranslation()
const [body, setBody] = useState<SlotBody>(() => const [body, setBody] = useState<SlotBody>(() =>
draft.slot ? toSlotBody(draft.slot) : emptyBody(draft.defaults), draft.slot ? toSlotBody(draft.slot) : emptyBody(draft.defaults),
@@ -14,11 +14,11 @@ export function TemplateIssues({
channelId, channelId,
slotsById, slotsById,
onGoToSlot, onGoToSlot,
}: { }: Readonly<{
channelId: string channelId: string
slotsById: Map<string, SlotDto> slotsById: Map<string, SlotDto>
onGoToSlot: (slot: SlotDto) => void onGoToSlot: (slot: SlotDto) => void
}) { }>) {
const { t } = useTranslation() const { t } = useTranslation()
const { data: issues } = useQuery({ const { data: issues } = useQuery({
queryKey: qk.channels.issues(channelId), queryKey: qk.channels.issues(channelId),
@@ -53,11 +53,11 @@ function IssueRow({
issue, issue,
slot, slot,
onGoToSlot, onGoToSlot,
}: { }: Readonly<{
issue: TemplateIssueDto issue: TemplateIssueDto
slot: SlotDto | undefined slot: SlotDto | undefined
onGoToSlot: (slot: SlotDto) => void onGoToSlot: (slot: SlotDto) => void
}) { }>) {
const { t } = useTranslation() const { t } = useTranslation()
const Icon = issue.severity === 'Error' ? CircleAlert : AlertTriangle const Icon = issue.severity === 'Error' ? CircleAlert : AlertTriangle
@@ -27,7 +27,7 @@ const PROGRAMME_KINDS = new Set<PlannedItemKind>(['Program', 'Fallback', 'SignOf
* Предпросмотр (см. 6.4): прогон генератора по текущим правилам без записи и без продвижения * Предпросмотр (см. 6.4): прогон генератора по текущим правилам без записи и без продвижения
* курсоров. Заставки приходят резервом известной длины — реальный рендер только при применении. * курсоров. Заставки приходят резервом известной длины — реальный рендер только при применении.
*/ */
export function TemplatePreview({ channelId }: { channelId: string }) { export function TemplatePreview({ channelId }: Readonly<{ channelId: string }>) {
const { t } = useTranslation() const { t } = useTranslation()
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false)
const [days, setDays] = useState(1) 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 { t } = useTranslation()
const items = preview.items.filter((i) => PROGRAMME_KINDS.has(i.kind)) 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 })) .map(([hour, minutes]) => ({ hour: new Date(hour), minutes }))
} }
function Tape({ preview }: { preview: SchedulePreviewDto }) { function Tape({ preview }: Readonly<{ preview: SchedulePreviewDto }>) {
const { t } = useTranslation() const { t } = useTranslation()
const load = useMemo(() => loadByHour(preview), [preview]) const load = useMemo(() => loadByHour(preview), [preview])
const peak = Math.max(1, ...load.map((l) => l.minutes)) 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 { t } = useTranslation()
const minutes = const minutes =
(new Date(item.endsAtUtc).getTime() - new Date(item.startsAtUtc).getTime()) / 60_000 (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() const { t } = useTranslation()
// Ключ строки — «вид + порядковый номер внутри вида»: у предупреждения нет своего id, а текст // Ключ строки — «вид + порядковый номер внутри вида»: у предупреждения нет своего 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 { t } = useTranslation()
const { days, rows } = useMemo(() => { const { days, rows } = useMemo(() => {
@@ -21,12 +21,12 @@ export function ViewerCard({
bare, bare,
onSaved, onSaved,
onError, onError,
}: { }: Readonly<{
channel: ChannelDto channel: ChannelDto
bare?: boolean bare?: boolean
onSaved: () => void onSaved: () => void
onError: (error: unknown) => void onError: (error: unknown) => void
}) { }>) {
const { t } = useTranslation() const { t } = useTranslation()
const [viewer, setViewer] = useState<ViewerSettings>(channel.viewer) const [viewer, setViewer] = useState<ViewerSettings>(channel.viewer)
const [galleryOpen, setGalleryOpen] = useState(false) const [galleryOpen, setGalleryOpen] = useState(false)
@@ -22,7 +22,7 @@ import {
updateCollection, updateCollection,
} from './api' } from './api'
export function CollectionDetail({ collectionId }: { collectionId: string }) { export function CollectionDetail({ collectionId }: Readonly<{ collectionId: string }>) {
const { t } = useTranslation() const { t } = useTranslation()
const queryClient = useQueryClient() const queryClient = useQueryClient()
const [galleryOpen, setGalleryOpen] = useState(false) const [galleryOpen, setGalleryOpen] = useState(false)
@@ -73,12 +73,12 @@ export function GenresPanel() {
mutationFn: ({ mutationFn: ({
id, id,
...body ...body
}: { }: Readonly<{
id: string id: string
name: string name: string
sortOrder: number sortOrder: number
aliases: string[] aliases: string[]
}) => updateGenre(id, body), }>) => updateGenre(id, body),
onSuccess: invalidate, onSuccess: invalidate,
}) })
const deleteMutation = useMutation({ const deleteMutation = useMutation({
@@ -2,7 +2,7 @@ import { useTranslation } from 'react-i18next'
import { splitDuration } from './format' import { splitDuration } from './format'
/** Объём эфира: «118 ч 40 мин». Ноль — прочерк, потому что «0 ч» читается как сбой подсчёта. */ /** Объём эфира: «118 ч 40 мин». Ноль — прочерк, потому что «0 ч» читается как сбой подсчёта. */
export function DurationLabel({ seconds }: { seconds: number }) { export function DurationLabel({ seconds }: Readonly<{ seconds: number }>) {
const { t } = useTranslation() const { t } = useTranslation()
const parts = splitDuration(seconds) const parts = splitDuration(seconds)
if (!parts) return <></> if (!parts) return <></>
@@ -25,7 +25,7 @@ import { GroupFilterPanel } from './GroupFilterPanel'
const EMPTY_FILTER: GroupFilter = {} const EMPTY_FILTER: GroupFilter = {}
export function GroupDetail({ groupId }: { groupId: string }) { export function GroupDetail({ groupId }: Readonly<{ groupId: string }>) {
const { t } = useTranslation() const { t } = useTranslation()
const queryClient = useQueryClient() const queryClient = useQueryClient()
@@ -88,7 +88,7 @@ export function GroupDetail({ groupId }: { groupId: string }) {
onError, onError,
}) })
const weightMutation = useMutation({ const weightMutation = useMutation({
mutationFn: ({ itemId, weight }: { itemId: string; weight: number }) => mutationFn: ({ itemId, weight }: Readonly<{ itemId: string; weight: number }>) =>
setGroupItemWeight(groupId, itemId, weight), setGroupItemWeight(groupId, itemId, weight),
onSuccess: invalidate, onSuccess: invalidate,
onError, onError,
@@ -14,10 +14,10 @@ const SHOW_KINDS: ShowKind[] = ['Series', 'Single']
export function GroupFilterPanel({ export function GroupFilterPanel({
filter, filter,
onChange, onChange,
}: { }: Readonly<{
filter: GroupFilter filter: GroupFilter
onChange: (next: GroupFilter) => void onChange: (next: GroupFilter) => void
}) { }>) {
const { t } = useTranslation() const { t } = useTranslation()
const { data: genres } = useQuery({ queryKey: qk.genres.all, queryFn: listGenres }) const { data: genres } = useQuery({ queryKey: qk.genres.all, queryFn: listGenres })
@@ -25,11 +25,11 @@ export function GalleryBrowser({
category = 'Library', category = 'Library',
onSelect, onSelect,
onClose, onClose,
}: { }: Readonly<{
category?: ImageCategory category?: ImageCategory
onSelect?: (image: ImagePick) => void onSelect?: (image: ImagePick) => void
onClose?: () => void onClose?: () => void
}) { }>) {
const { t } = useTranslation() const { t } = useTranslation()
const queryClient = useQueryClient() const queryClient = useQueryClient()
const [active, setActive] = useState<ImageCategory>(category) const [active, setActive] = useState<ImageCategory>(category)
@@ -193,12 +193,12 @@ export function ImageGallery({
onOpenChange, onOpenChange,
category, category,
onSelect, onSelect,
}: { }: Readonly<{
open: boolean open: boolean
onOpenChange: (open: boolean) => void onOpenChange: (open: boolean) => void
category?: ImageCategory category?: ImageCategory
onSelect?: (image: ImagePick) => void onSelect?: (image: ImagePick) => void
}) { }>) {
const { t } = useTranslation() const { t } = useTranslation()
return ( return (
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog open={open} onOpenChange={onOpenChange}>
@@ -16,10 +16,10 @@ import { formatClock } from './format'
export function BlockBuilder({ export function BlockBuilder({
onSaved, onSaved,
onError, onError,
}: { }: Readonly<{
onSaved: () => void onSaved: () => void
onError: (error: unknown) => void onError: (error: unknown) => void
}) { }>) {
const { t } = useTranslation() const { t } = useTranslation()
const [name, setName] = useState('') const [name, setName] = useState('')
const [items, setItems] = useState<ClipDragItem[]>([]) const [items, setItems] = useState<ClipDragItem[]>([])
@@ -15,7 +15,7 @@ import { formatClock } from './format'
* Группы роликов собираются здесь же, а не в общем редакторе групп (см. 6.7): в выбранную группу * Группы роликов собираются здесь же, а не в общем редакторе групп (см. 6.7): в выбранную группу
* перетаскиваются и отдельные ролики, и готовые блоки — стык умеет и то и другое. * перетаскиваются и отдельные ролики, и готовые блоки — стык умеет и то и другое.
*/ */
export function ClipGroupPanel({ onError }: { onError: (error: unknown) => void }) { export function ClipGroupPanel({ onError }: Readonly<{ onError: (error: unknown) => void }>) {
const { t } = useTranslation() const { t } = useTranslation()
const queryClient = useQueryClient() const queryClient = useQueryClient()
const [selected, setSelected] = useState('') const [selected, setSelected] = useState('')
@@ -48,7 +48,7 @@ export function InterstitialsPanel() {
const onError = useApiError() const onError = useApiError()
const renameMutation = useMutation({ 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: () => { onSuccess: () => {
setRenaming(null) setRenaming(null)
invalidate() 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 { t } = useTranslation()
const queryClient = useQueryClient() const queryClient = useQueryClient()
const [selected, setSelected] = useState<string[]>([]) 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() const { t } = useTranslation()
return ( return (
<tr className="border-b border-border last:border-0"> <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 { cn } from '@/shared/lib/cn'
import { type UploadItem, useUploadStore } from './upload-store' import { type UploadItem, useUploadStore } from './upload-store'
function StatusIcon({ status }: { status: UploadItem['status'] }) { function StatusIcon({ status }: Readonly<{ status: UploadItem['status'] }>) {
switch (status) { switch (status) {
case 'done': case 'done':
return <Check className="h-3.5 w-3.5 shrink-0 text-primary" /> return <Check className="h-3.5 w-3.5 shrink-0 text-primary" />
@@ -23,7 +23,10 @@ import { useUploadStore } from './upload-store'
/** Radix Select запрещает пустое значение — под «в библиотеку» используем спец-значение. */ /** Radix Select запрещает пустое значение — под «в библиотеку» используем спец-значение. */
const LIBRARY_VALUE = '__library__' 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 { t } = useTranslation()
const enqueue = useUploadStore((s) => s.enqueue) const enqueue = useUploadStore((s) => s.enqueue)
const [seasonStr, setSeasonStr] = useState('') const [seasonStr, setSeasonStr] = useState('')
@@ -50,7 +50,7 @@ export function RolesPanel() {
}) })
const renameMutation = useMutation({ 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, onSuccess: invalidate,
onError, onError,
}) })
@@ -44,7 +44,7 @@ function addButtonLabel(
return `${t('admin.shows.addSelected')} (${count})` return `${t('admin.shows.addSelected')} (${count})`
} }
export function ShowDetail({ showId }: { showId: string }) { export function ShowDetail({ showId }: Readonly<{ showId: string }>) {
const { t } = useTranslation() const { t } = useTranslation()
const queryClient = useQueryClient() const queryClient = useQueryClient()
const [filter, setFilter] = useState('') 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 { t } = useTranslation()
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false)
const [selected, setSelected] = useState<string[]>([]) const [selected, setSelected] = useState<string[]>([])
@@ -28,7 +28,10 @@ import {
updateMetadata, updateMetadata,
} from './api' } 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 { t } = useTranslation()
const [galleryOpen, setGalleryOpen] = useState(false) const [galleryOpen, setGalleryOpen] = useState(false)
const [provider, setProvider] = useState('') 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() const { t } = useTranslation()
if (gap.expected == null) if (gap.expected == null)
@@ -64,7 +64,7 @@ export function UsersPanel() {
const unblockMutation = useMutation({ mutationFn: unblockUser, onSuccess: invalidate, onError }) const unblockMutation = useMutation({ mutationFn: unblockUser, onSuccess: invalidate, onError })
const deleteMutation = useMutation({ mutationFn: deleteUser, onSuccess: invalidate, onError }) const deleteMutation = useMutation({ mutationFn: deleteUser, onSuccess: invalidate, onError })
const changeRoleMutation = useMutation({ const changeRoleMutation = useMutation({
mutationFn: ({ userId, roleId: nextRoleId }: { userId: string; roleId: string }) => mutationFn: ({ userId, roleId: nextRoleId }: Readonly<{ userId: string; roleId: string }>) =>
changeUserRole(userId, nextRoleId), changeUserRole(userId, nextRoleId),
onSuccess: invalidate, onSuccess: invalidate,
onError, onError,
@@ -302,11 +302,11 @@ function ResetPasswordDialog({
user, user,
onClose, onClose,
onError, onError,
}: { }: Readonly<{
user: UserSummaryDto user: UserSummaryDto
onClose: () => void onClose: () => void
onError: (e: unknown) => void onError: (e: unknown) => void
}) { }>) {
const { t } = useTranslation() const { t } = useTranslation()
const [password, setPassword] = useState('') const [password, setPassword] = useState('')
+1 -1
View File
@@ -16,7 +16,7 @@ const schema = z.object({
type FormValues = z.infer<typeof schema> type FormValues = z.infer<typeof schema>
export function LoginForm({ onSuccess }: { onSuccess: () => void }) { export function LoginForm({ onSuccess }: Readonly<{ onSuccess: () => void }>) {
const { t } = useTranslation() const { t } = useTranslation()
const { const {
register: registerField, register: registerField,
+1 -1
View File
@@ -16,7 +16,7 @@ const schema = z.object({
type FormValues = z.infer<typeof schema> type FormValues = z.infer<typeof schema>
export function RegisterForm({ onSuccess }: { onSuccess: () => void }) { export function RegisterForm({ onSuccess }: Readonly<{ onSuccess: () => void }>) {
const { t } = useTranslation() const { t } = useTranslation()
const { const {
register: registerField, register: registerField,
+1 -1
View File
@@ -15,7 +15,7 @@ function formatTime(iso: string) {
} }
/** Кадр серии, если он есть; иначе постер шоу (он вертикальный, отсюда другая ширина). */ /** Кадр серии, если он есть; иначе постер шоу (он вертикальный, отсюда другая ширина). */
function EntryThumb({ entry }: { entry: PublicEpgEntryDto | undefined }) { function EntryThumb({ entry }: Readonly<{ entry: PublicEpgEntryDto | undefined }>) {
if (entry?.episodeStillImageId) if (entry?.episodeStillImageId)
return ( return (
<img <img
@@ -58,7 +58,7 @@ export function ChannelPlayer({
nextUp, nextUp,
flash, flash,
onUnavailable, onUnavailable,
}: { }: Readonly<{
slug: string slug: string
/** Канал, чьи оверлеи рисуем. Всё опционально: канал без логотипа и без шума — норма. */ /** Канал, чьи оверлеи рисуем. Всё опционально: канал без логотипа и без шума — норма. */
channel?: PublicChannelDto channel?: PublicChannelDto
@@ -67,7 +67,7 @@ export function ChannelPlayer({
/** Показать чёрный кадр с номером — переключение по номерам, как на телевизоре. */ /** Показать чёрный кадр с номером — переключение по номерам, как на телевизоре. */
flash?: boolean flash?: boolean
onUnavailable?: () => void onUnavailable?: () => void
}) { }>) {
const { t } = useTranslation() const { t } = useTranslation()
const containerRef = useRef<HTMLDivElement>(null) const containerRef = useRef<HTMLDivElement>(null)
const videoRef = useRef<HTMLVideoElement>(null) const videoRef = useRef<HTMLVideoElement>(null)
@@ -19,11 +19,11 @@ export function ChannelLogo({
imageId, imageId,
corner, corner,
opacity, opacity,
}: { }: Readonly<{
imageId: string imageId: string
corner: LogoCorner corner: LogoCorner
opacity: number opacity: number
}) { }>) {
return ( return (
<img <img
src={imageUrl(imageId)} src={imageUrl(imageId)}
@@ -52,7 +52,7 @@ export function ScreenClock() {
} }
/** Плашка «Далее: …» — данные уже есть в EPG, отдельного запроса не нужно. */ /** Плашка «Далее: …» — данные уже есть в EPG, отдельного запроса не нужно. */
export function NextUpBanner({ title }: { title: string }) { export function NextUpBanner({ title }: Readonly<{ title: string }>) {
const { t } = useTranslation() const { t } = useTranslation()
return ( return (
<span className="pointer-events-none absolute bottom-14 left-3 rounded bg-black/60 px-2 py-1 text-sm text-white"> <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-шум, дрожание и размытие краёв. Переборщить очень легко, поэтому * Аналоговый фильтр: лёгкий VHS-шум, дрожание и размытие краёв. Переборщить очень легко, поэтому
* сила регулируется, а вклад каждого слоя от неё убывает нелинейно. * сила регулируется, а вклад каждого слоя от неё убывает нелинейно.
*/ */
export function AnalogFilter({ strength }: { strength: number }) { export function AnalogFilter({ strength }: Readonly<{ strength: number }>) {
const s = Math.min(1, Math.max(0, strength)) const s = Math.min(1, Math.max(0, strength))
return ( 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 ( return (
<div className="pointer-events-none absolute inset-0 flex items-start justify-end bg-black"> <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"> <span className="m-6 flex items-baseline gap-2 text-white">
+1 -1
View File
@@ -8,7 +8,7 @@ import {
let nextId = 1 let nextId = 1
export function ToastProvider({ children }: { children: ReactNode }) { export function ToastProvider({ children }: Readonly<{ children: ReactNode }>) {
const [toasts, setToasts] = useState<ToastItem[]>([]) const [toasts, setToasts] = useState<ToastItem[]>([])
const push = useCallback((message: string, variant: ToastVariant) => { const push = useCallback((message: string, variant: ToastVariant) => {
+1 -1
View File
@@ -18,6 +18,6 @@ const badgeVariants = cva(
export type BadgeProps = HTMLAttributes<HTMLSpanElement> & VariantProps<typeof badgeVariants> export type BadgeProps = HTMLAttributes<HTMLSpanElement> & VariantProps<typeof badgeVariants>
export function Badge({ className, variant, ...props }: BadgeProps) { export function Badge({ className, variant, ...props }: Readonly<BadgeProps>) {
return <span className={cn(badgeVariants({ variant }), className)} {...props} /> return <span className={cn(badgeVariants({ variant }), className)} {...props} />
} }
+1 -1
View File
@@ -7,7 +7,7 @@ import { cn } from '@/shared/lib/cn'
* Мини-плеер HLS для админки: плейлист и сегменты лежат под admin-роутами (JWT), поэтому запросы * Мини-плеер HLS для админки: плейлист и сегменты лежат под admin-роутами (JWT), поэтому запросы
* идут через hls.js с Bearer-заголовком. Нативный путь (Safari) только там, где hls.js не нужен. * идут через hls.js с Bearer-заголовком. Нативный путь (Safari) только там, где hls.js не нужен.
*/ */
export function HlsVideo({ src, className }: { src: string; className?: string }) { export function HlsVideo({ src, className }: Readonly<{ src: string; className?: string }>) {
const videoRef = useRef<HTMLVideoElement>(null) const videoRef = useRef<HTMLVideoElement>(null)
useEffect(() => { useEffect(() => {
+2 -2
View File
@@ -6,11 +6,11 @@ export function Pager({
page, page,
totalPages, totalPages,
onChange, onChange,
}: { }: Readonly<{
page: number page: number
totalPages: number totalPages: number
onChange: (page: number) => void onChange: (page: number) => void
}) { }>) {
const { t } = useTranslation() const { t } = useTranslation()
if (totalPages <= 1) return null if (totalPages <= 1) return null
+2 -2
View File
@@ -9,13 +9,13 @@ export function SortHeader({
sort, sort,
onToggle, onToggle,
className, className,
}: { }: Readonly<{
label: string label: string
sortKey: string sortKey: string
sort: SortState sort: SortState
onToggle: (key: string) => void onToggle: (key: string) => void
className?: string className?: string
}) { }>) {
const active = sort.key === sortKey const active = sort.key === sortKey
const direction = sort.desc ? 'descending' : 'ascending' const direction = sort.desc ? 'descending' : 'ascending'
let Icon = ChevronsUpDown let Icon = ChevronsUpDown
+2 -2
View File
@@ -21,12 +21,12 @@ function ToastItem({
message, message,
variant, variant,
onDismiss, onDismiss,
}: { }: Readonly<{
id: number id: number
message: string message: string
variant: 'default' | 'success' | 'error' variant: 'default' | 'success' | 'error'
onDismiss: (id: number) => void onDismiss: (id: number) => void
}) { }>) {
const { t } = useTranslation() const { t } = useTranslation()
useEffect(() => { useEffect(() => {
+1 -1
View File
@@ -13,7 +13,7 @@ function applyTheme(theme: Theme) {
root.classList.toggle('dark', resolve(theme) === 'dark') root.classList.toggle('dark', resolve(theme) === 'dark')
} }
export function ThemeProvider({ children }: { children: ReactNode }) { export function ThemeProvider({ children }: Readonly<{ children: ReactNode }>) {
const [theme, setThemeState] = useState<Theme>( const [theme, setThemeState] = useState<Theme>(
() => (localStorage.getItem(THEME_STORAGE_KEY) as Theme | null) ?? 'dark', () => (localStorage.getItem(THEME_STORAGE_KEY) as Theme | null) ?? 'dark',
) )