From 5449c05b5b95e5046e235c9429b7dd01733ed7ed Mon Sep 17 00:00:00 2001 From: Leonid Pershin Date: Mon, 27 Jul 2026 01:14:53 +0300 Subject: [PATCH] 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. --- .../TeleWave.Application/Common/Models/Result.cs | 4 ++-- .../Programming/Planning/GroupExpander.cs | 4 ++-- .../Programming/Templates/PlanningRules.cs | 8 ++------ .../Identity/RoleService.cs | 2 +- .../src/features/admin/channels/ChannelDetail.tsx | 4 ++-- .../admin/channels/components/ApplyDialog.tsx | 4 ++-- .../channels/components/BumperBackgroundField.tsx | 4 ++-- .../admin/channels/components/BumperCard.tsx | 4 ++-- .../channels/components/BumperFileUpload.tsx | 4 ++-- .../channels/components/BumperPreviewPlayer.tsx | 4 ++-- .../channels/components/BumperTemplateEditor.tsx | 4 ++-- .../channels/components/BumperVariantEditor.tsx | 4 ++-- .../admin/channels/components/CollapsibleCard.tsx | 4 ++-- .../channels/components/EntryTraceDialog.tsx | 6 +++--- .../admin/channels/components/GridTab.tsx | 12 ++++++------ .../channels/components/JunctionElementDialog.tsx | 4 ++-- .../admin/channels/components/JunctionsCard.tsx | 8 ++++---- .../components/LayerApplicabilityDialog.tsx | 12 ++++++------ .../admin/channels/components/RulesCard.tsx | 4 ++-- .../admin/channels/components/ScheduleGrid.tsx | 8 ++++---- .../admin/channels/components/SchedulePreview.tsx | 8 ++++---- .../admin/channels/components/SettingsCard.tsx | 4 ++-- .../admin/channels/components/SlotInspector.tsx | 4 ++-- .../admin/channels/components/TemplateIssues.tsx | 8 ++++---- .../admin/channels/components/TemplatePreview.tsx | 15 +++++++++------ .../admin/channels/components/ViewerCard.tsx | 4 ++-- .../admin/collections/CollectionDetail.tsx | 2 +- .../src/features/admin/genres/GenresPanel.tsx | 4 ++-- .../src/features/admin/groups/DurationLabel.tsx | 2 +- .../src/features/admin/groups/GroupDetail.tsx | 4 ++-- .../features/admin/groups/GroupFilterPanel.tsx | 4 ++-- .../src/features/admin/images/ImageGallery.tsx | 8 ++++---- .../features/admin/interstitials/BlockBuilder.tsx | 4 ++-- .../admin/interstitials/ClipGroupPanel.tsx | 2 +- .../admin/interstitials/InterstitialsPanel.tsx | 2 +- .../features/admin/media/ManualInboxDialog.tsx | 2 +- frontend/src/features/admin/media/MediaPanel.tsx | 2 +- .../src/features/admin/media/UploadSnackbar.tsx | 2 +- .../features/admin/media/UploadToShowDialog.tsx | 5 ++++- frontend/src/features/admin/roles/RolesPanel.tsx | 2 +- frontend/src/features/admin/shows/ShowDetail.tsx | 2 +- .../src/features/admin/shows/ShowGenresField.tsx | 5 ++++- .../src/features/admin/shows/ShowMetadataCard.tsx | 7 +++++-- frontend/src/features/admin/users/UsersPanel.tsx | 6 +++--- frontend/src/features/auth/LoginForm.tsx | 2 +- frontend/src/features/auth/RegisterForm.tsx | 2 +- frontend/src/features/streaming/AirPage.tsx | 2 +- frontend/src/features/streaming/ChannelPlayer.tsx | 4 ++-- .../src/features/streaming/PlayerOverlays.tsx | 10 +++++----- frontend/src/shared/ui/ToastProvider.tsx | 2 +- frontend/src/shared/ui/badge.tsx | 2 +- frontend/src/shared/ui/hls-video.tsx | 2 +- frontend/src/shared/ui/pager.tsx | 4 ++-- frontend/src/shared/ui/sortable.tsx | 4 ++-- frontend/src/shared/ui/toaster.tsx | 4 ++-- frontend/src/theme/ThemeProvider.tsx | 2 +- 56 files changed, 132 insertions(+), 124 deletions(-) diff --git a/backend/src/TeleWave.Application/Common/Models/Result.cs b/backend/src/TeleWave.Application/Common/Models/Result.cs index 17a96dc..d6c6a13 100644 --- a/backend/src/TeleWave.Application/Common/Models/Result.cs +++ b/backend/src/TeleWave.Application/Common/Models/Result.cs @@ -18,10 +18,10 @@ public class Result public static Result Success() => new(true, Error.None); - public static Result Failure(Error error) => new(false, error); - public static Result Success(T value) => new(value, true, Error.None); + public static Result Failure(Error error) => new(false, error); + public static Result Failure(Error error) => new(default, false, error); } diff --git a/backend/src/TeleWave.Application/Programming/Planning/GroupExpander.cs b/backend/src/TeleWave.Application/Programming/Planning/GroupExpander.cs index ff3d8da..da34fbc 100644 --- a/backend/src/TeleWave.Application/Programming/Planning/GroupExpander.cs +++ b/backend/src/TeleWave.Application/Programming/Planning/GroupExpander.cs @@ -195,7 +195,7 @@ public sealed class GroupExpander(IAppDbContext dbContext) e.ChannelId == channelId && e.Kind == ScheduleEntryKind.Program && e.ShowId != null - && showIds.Contains(e.ShowId!.Value) + && showIds.Contains(e.ShowId.Value) ) .GroupBy(e => e.ShowId!.Value) .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.Kind == ScheduleEntryKind.Program && e.ShowId != null - && showIds.Contains(e.ShowId!.Value) + && showIds.Contains(e.ShowId.Value) && e.StartsAtUtc >= since ) .Select(e => new { ShowId = e.ShowId!.Value, e.StartsAtUtc }) diff --git a/backend/src/TeleWave.Application/Programming/Templates/PlanningRules.cs b/backend/src/TeleWave.Application/Programming/Templates/PlanningRules.cs index e01f104..75aec3b 100644 --- a/backend/src/TeleWave.Application/Programming/Templates/PlanningRules.cs +++ b/backend/src/TeleWave.Application/Programming/Templates/PlanningRules.cs @@ -50,12 +50,8 @@ public sealed record PlanningRules( // Пересекающиеся окна разрешаются в пользу строгого: детское время не должно // отменяться более широким окном, случайно наложенным сверху. - ShowAudience? strictest = null; - foreach (var window in windows.Where(w => w.Contains(moment))) - strictest = strictest is { } current && current <= window.MaxAudience - ? current - : window.MaxAudience; - return strictest; + var applicable = windows.Where(w => w.Contains(moment)).Select(w => w.MaxAudience).ToList(); + return applicable.Count > 0 ? applicable.Min() : null; } public string ToJson() => JsonSerializer.Serialize(this, Options); diff --git a/backend/src/TeleWave.Infrastructure/Identity/RoleService.cs b/backend/src/TeleWave.Infrastructure/Identity/RoleService.cs index 3e3ff01..85f3ee4 100644 --- a/backend/src/TeleWave.Infrastructure/Identity/RoleService.cs +++ b/backend/src/TeleWave.Infrastructure/Identity/RoleService.cs @@ -110,7 +110,7 @@ internal sealed class RoleService( if (currentRoles.Count > 0) await userManager.RemoveFromRolesAsync(user, currentRoles); - await userManager.AddToRoleAsync(user, role.Name!); + await userManager.AddToRoleAsync(user, role.Name); return Result.Success(); } diff --git a/frontend/src/features/admin/channels/ChannelDetail.tsx b/frontend/src/features/admin/channels/ChannelDetail.tsx index f894fca..39789d9 100644 --- a/frontend/src/features/admin/channels/ChannelDetail.tsx +++ b/frontend/src/features/admin/channels/ChannelDetail.tsx @@ -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 = () => { diff --git a/frontend/src/features/admin/channels/components/ApplyDialog.tsx b/frontend/src/features/admin/channels/components/ApplyDialog.tsx index 576bf1e..5851f98 100644 --- a/frontend/src/features/admin/channels/components/ApplyDialog.tsx +++ b/frontend/src/features/admin/channels/components/ApplyDialog.tsx @@ -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), diff --git a/frontend/src/features/admin/channels/components/BumperBackgroundField.tsx b/frontend/src/features/admin/channels/components/BumperBackgroundField.tsx index 1381db7..a343d77 100644 --- a/frontend/src/features/admin/channels/components/BumperBackgroundField.tsx +++ b/frontend/src/features/admin/channels/components/BumperBackgroundField.tsx @@ -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) diff --git a/frontend/src/features/admin/channels/components/BumperCard.tsx b/frontend/src/features/admin/channels/components/BumperCard.tsx index d8aacc1..4b1066a 100644 --- a/frontend/src/features/admin/channels/components/BumperCard.tsx +++ b/frontend/src/features/admin/channels/components/BumperCard.tsx @@ -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(channel.bumper) diff --git a/frontend/src/features/admin/channels/components/BumperFileUpload.tsx b/frontend/src/features/admin/channels/components/BumperFileUpload.tsx index 0485987..c1e833c 100644 --- a/frontend/src/features/admin/channels/components/BumperFileUpload.tsx +++ b/frontend/src/features/admin/channels/components/BumperFileUpload.tsx @@ -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 onSaved: () => void onError: (e: unknown) => void -}) { +}>) { const { t } = useTranslation() const inputId = `bumper-${kind}-${templateId}` diff --git a/frontend/src/features/admin/channels/components/BumperPreviewPlayer.tsx b/frontend/src/features/admin/channels/components/BumperPreviewPlayer.tsx index ce18032..a78e72a 100644 --- a/frontend/src/features/admin/channels/components/BumperPreviewPlayer.tsx +++ b/frontend/src/features/admin/channels/components/BumperPreviewPlayer.tsx @@ -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) diff --git a/frontend/src/features/admin/channels/components/BumperTemplateEditor.tsx b/frontend/src/features/admin/channels/components/BumperTemplateEditor.tsx index cbe1d96..ab08b52 100644 --- a/frontend/src/features/admin/channels/components/BumperTemplateEditor.tsx +++ b/frontend/src/features/admin/channels/components/BumperTemplateEditor.tsx @@ -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) diff --git a/frontend/src/features/admin/channels/components/BumperVariantEditor.tsx b/frontend/src/features/admin/channels/components/BumperVariantEditor.tsx index 9fca9ba..322da6d 100644 --- a/frontend/src/features/admin/channels/components/BumperVariantEditor.tsx +++ b/frontend/src/features/admin/channels/components/BumperVariantEditor.tsx @@ -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, diff --git a/frontend/src/features/admin/channels/components/CollapsibleCard.tsx b/frontend/src/features/admin/channels/components/CollapsibleCard.tsx index 62971c4..3d83e98 100644 --- a/frontend/src/features/admin/channels/components/CollapsibleCard.tsx +++ b/frontend/src/features/admin/channels/components/CollapsibleCard.tsx @@ -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) diff --git a/frontend/src/features/admin/channels/components/EntryTraceDialog.tsx b/frontend/src/features/admin/channels/components/EntryTraceDialog.tsx index 6d9b3e2..831e415 100644 --- a/frontend/src/features/admin/channels/components/EntryTraceDialog.tsx +++ b/frontend/src/features/admin/channels/components/EntryTraceDialog.tsx @@ -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 ( <>
{label}
diff --git a/frontend/src/features/admin/channels/components/GridTab.tsx b/frontend/src/features/admin/channels/components/GridTab.tsx index 0d9b378..78af3a7 100644 --- a/frontend/src/features/admin/channels/components/GridTab.tsx +++ b/frontend/src/features/admin/channels/components/GridTab.tsx @@ -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(null) const [activeLayerId, setActiveLayerId] = useState(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 })), ) diff --git a/frontend/src/features/admin/channels/components/JunctionElementDialog.tsx b/frontend/src/features/admin/channels/components/JunctionElementDialog.tsx index 9b02097..816de1e 100644 --- a/frontend/src/features/admin/channels/components/JunctionElementDialog.tsx +++ b/frontend/src/features/admin/channels/components/JunctionElementDialog.tsx @@ -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(() => toBody(element)) const { data: groups } = useQuery({ queryKey: qk.groups.all, queryFn: listGroups }) diff --git a/frontend/src/features/admin/channels/components/JunctionsCard.tsx b/frontend/src/features/admin/channels/components/JunctionsCard.tsx index 5a7c11b..9765832 100644 --- a/frontend/src/features/admin/channels/components/JunctionsCard.tsx +++ b/frontend/src/features/admin/channels/components/JunctionsCard.tsx @@ -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(null) const [dragged, setDragged] = useState(null) diff --git a/frontend/src/features/admin/channels/components/LayerApplicabilityDialog.tsx b/frontend/src/features/admin/channels/components/LayerApplicabilityDialog.tsx index c2df1a9..e2a73b2 100644 --- a/frontend/src/features/admin/channels/components/LayerApplicabilityDialog.tsx +++ b/frontend/src/features/admin/channels/components/LayerApplicabilityDialog.tsx @@ -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(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 (
@@ -228,11 +228,11 @@ function MonthDay({ value, prefix, onChange, -}: { +}: Readonly<{ value: AnnualRange prefix: 'from' | 'to' onChange: (part: Partial) => void -}) { +}>) { const month = prefix === 'from' ? value.fromMonth : value.toMonth const day = prefix === 'from' ? value.fromDay : value.toDay diff --git a/frontend/src/features/admin/channels/components/RulesCard.tsx b/frontend/src/features/admin/channels/components/RulesCard.tsx index b029781..3d872d1 100644 --- a/frontend/src/features/admin/channels/components/RulesCard.tsx +++ b/frontend/src/features/admin/channels/components/RulesCard.tsx @@ -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(() => toRows(template.rules?.maxAudienceByTime ?? []), diff --git a/frontend/src/features/admin/channels/components/ScheduleGrid.tsx b/frontend/src/features/admin/channels/components/ScheduleGrid.tsx index eaa81c7..c0e9288 100644 --- a/frontend/src/features/admin/channels/components/ScheduleGrid.tsx +++ b/frontend/src/features/admin/channels/components/ScheduleGrid.tsx @@ -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(null) diff --git a/frontend/src/features/admin/channels/components/SchedulePreview.tsx b/frontend/src/features/admin/channels/components/SchedulePreview.tsx index df162a1..cd4f496 100644 --- a/frontend/src/features/admin/channels/components/SchedulePreview.tsx +++ b/frontend/src/features/admin/channels/components/SchedulePreview.tsx @@ -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 {t('air.ad')} @@ -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

{t('admin.channels.noSchedule')}

diff --git a/frontend/src/features/admin/channels/components/SettingsCard.tsx b/frontend/src/features/admin/channels/components/SettingsCard.tsx index 00a6c7b..417b9e5 100644 --- a/frontend/src/features/admin/channels/components/SettingsCard.tsx +++ b/frontend/src/features/admin/channels/components/SettingsCard.tsx @@ -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) diff --git a/frontend/src/features/admin/channels/components/SlotInspector.tsx b/frontend/src/features/admin/channels/components/SlotInspector.tsx index 3baee36..116b45e 100644 --- a/frontend/src/features/admin/channels/components/SlotInspector.tsx +++ b/frontend/src/features/admin/channels/components/SlotInspector.tsx @@ -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(() => draft.slot ? toSlotBody(draft.slot) : emptyBody(draft.defaults), diff --git a/frontend/src/features/admin/channels/components/TemplateIssues.tsx b/frontend/src/features/admin/channels/components/TemplateIssues.tsx index c769f0a..34eb50f 100644 --- a/frontend/src/features/admin/channels/components/TemplateIssues.tsx +++ b/frontend/src/features/admin/channels/components/TemplateIssues.tsx @@ -14,11 +14,11 @@ export function TemplateIssues({ channelId, slotsById, onGoToSlot, -}: { +}: Readonly<{ channelId: string slotsById: Map 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 diff --git a/frontend/src/features/admin/channels/components/TemplatePreview.tsx b/frontend/src/features/admin/channels/components/TemplatePreview.tsx index efdbf68..e854142 100644 --- a/frontend/src/features/admin/channels/components/TemplatePreview.tsx +++ b/frontend/src/features/admin/channels/components/TemplatePreview.tsx @@ -27,7 +27,7 @@ const PROGRAMME_KINDS = new Set(['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(() => { diff --git a/frontend/src/features/admin/channels/components/ViewerCard.tsx b/frontend/src/features/admin/channels/components/ViewerCard.tsx index 3a3c1c2..bed76ab 100644 --- a/frontend/src/features/admin/channels/components/ViewerCard.tsx +++ b/frontend/src/features/admin/channels/components/ViewerCard.tsx @@ -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(channel.viewer) const [galleryOpen, setGalleryOpen] = useState(false) diff --git a/frontend/src/features/admin/collections/CollectionDetail.tsx b/frontend/src/features/admin/collections/CollectionDetail.tsx index fef2f71..db41172 100644 --- a/frontend/src/features/admin/collections/CollectionDetail.tsx +++ b/frontend/src/features/admin/collections/CollectionDetail.tsx @@ -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) diff --git a/frontend/src/features/admin/genres/GenresPanel.tsx b/frontend/src/features/admin/genres/GenresPanel.tsx index eaa7fb1..34e04f1 100644 --- a/frontend/src/features/admin/genres/GenresPanel.tsx +++ b/frontend/src/features/admin/genres/GenresPanel.tsx @@ -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({ diff --git a/frontend/src/features/admin/groups/DurationLabel.tsx b/frontend/src/features/admin/groups/DurationLabel.tsx index 8f93b79..1803b9d 100644 --- a/frontend/src/features/admin/groups/DurationLabel.tsx +++ b/frontend/src/features/admin/groups/DurationLabel.tsx @@ -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 <>— diff --git a/frontend/src/features/admin/groups/GroupDetail.tsx b/frontend/src/features/admin/groups/GroupDetail.tsx index 12be56a..67abef4 100644 --- a/frontend/src/features/admin/groups/GroupDetail.tsx +++ b/frontend/src/features/admin/groups/GroupDetail.tsx @@ -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, diff --git a/frontend/src/features/admin/groups/GroupFilterPanel.tsx b/frontend/src/features/admin/groups/GroupFilterPanel.tsx index 58134ce..dccc6e0 100644 --- a/frontend/src/features/admin/groups/GroupFilterPanel.tsx +++ b/frontend/src/features/admin/groups/GroupFilterPanel.tsx @@ -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 }) diff --git a/frontend/src/features/admin/images/ImageGallery.tsx b/frontend/src/features/admin/images/ImageGallery.tsx index 715547e..c472714 100644 --- a/frontend/src/features/admin/images/ImageGallery.tsx +++ b/frontend/src/features/admin/images/ImageGallery.tsx @@ -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(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 ( diff --git a/frontend/src/features/admin/interstitials/BlockBuilder.tsx b/frontend/src/features/admin/interstitials/BlockBuilder.tsx index b956d4d..1f71413 100644 --- a/frontend/src/features/admin/interstitials/BlockBuilder.tsx +++ b/frontend/src/features/admin/interstitials/BlockBuilder.tsx @@ -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([]) diff --git a/frontend/src/features/admin/interstitials/ClipGroupPanel.tsx b/frontend/src/features/admin/interstitials/ClipGroupPanel.tsx index 64efbc4..dd34ff1 100644 --- a/frontend/src/features/admin/interstitials/ClipGroupPanel.tsx +++ b/frontend/src/features/admin/interstitials/ClipGroupPanel.tsx @@ -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('') diff --git a/frontend/src/features/admin/interstitials/InterstitialsPanel.tsx b/frontend/src/features/admin/interstitials/InterstitialsPanel.tsx index 5310f13..a52fd1f 100644 --- a/frontend/src/features/admin/interstitials/InterstitialsPanel.tsx +++ b/frontend/src/features/admin/interstitials/InterstitialsPanel.tsx @@ -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() diff --git a/frontend/src/features/admin/media/ManualInboxDialog.tsx b/frontend/src/features/admin/media/ManualInboxDialog.tsx index b3dbec3..e52a853 100644 --- a/frontend/src/features/admin/media/ManualInboxDialog.tsx +++ b/frontend/src/features/admin/media/ManualInboxDialog.tsx @@ -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([]) diff --git a/frontend/src/features/admin/media/MediaPanel.tsx b/frontend/src/features/admin/media/MediaPanel.tsx index 14be1f4..b78b7b7 100644 --- a/frontend/src/features/admin/media/MediaPanel.tsx +++ b/frontend/src/features/admin/media/MediaPanel.tsx @@ -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 ( diff --git a/frontend/src/features/admin/media/UploadSnackbar.tsx b/frontend/src/features/admin/media/UploadSnackbar.tsx index a659654..915fd49 100644 --- a/frontend/src/features/admin/media/UploadSnackbar.tsx +++ b/frontend/src/features/admin/media/UploadSnackbar.tsx @@ -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 diff --git a/frontend/src/features/admin/media/UploadToShowDialog.tsx b/frontend/src/features/admin/media/UploadToShowDialog.tsx index d8a2c9c..980e052 100644 --- a/frontend/src/features/admin/media/UploadToShowDialog.tsx +++ b/frontend/src/features/admin/media/UploadToShowDialog.tsx @@ -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('') diff --git a/frontend/src/features/admin/roles/RolesPanel.tsx b/frontend/src/features/admin/roles/RolesPanel.tsx index 8629dbd..c80e1bb 100644 --- a/frontend/src/features/admin/roles/RolesPanel.tsx +++ b/frontend/src/features/admin/roles/RolesPanel.tsx @@ -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, }) diff --git a/frontend/src/features/admin/shows/ShowDetail.tsx b/frontend/src/features/admin/shows/ShowDetail.tsx index 9c2dea2..4fe5815 100644 --- a/frontend/src/features/admin/shows/ShowDetail.tsx +++ b/frontend/src/features/admin/shows/ShowDetail.tsx @@ -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('') diff --git a/frontend/src/features/admin/shows/ShowGenresField.tsx b/frontend/src/features/admin/shows/ShowGenresField.tsx index 33ef482..6985ba9 100644 --- a/frontend/src/features/admin/shows/ShowGenresField.tsx +++ b/frontend/src/features/admin/shows/ShowGenresField.tsx @@ -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([]) diff --git a/frontend/src/features/admin/shows/ShowMetadataCard.tsx b/frontend/src/features/admin/shows/ShowMetadataCard.tsx index e5afa4b..afb0e3b 100644 --- a/frontend/src/features/admin/shows/ShowMetadataCard.tsx +++ b/frontend/src/features/admin/shows/ShowMetadataCard.tsx @@ -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) diff --git a/frontend/src/features/admin/users/UsersPanel.tsx b/frontend/src/features/admin/users/UsersPanel.tsx index c550f22..14fe914 100644 --- a/frontend/src/features/admin/users/UsersPanel.tsx +++ b/frontend/src/features/admin/users/UsersPanel.tsx @@ -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('') diff --git a/frontend/src/features/auth/LoginForm.tsx b/frontend/src/features/auth/LoginForm.tsx index 5bb53fe..2f88365 100644 --- a/frontend/src/features/auth/LoginForm.tsx +++ b/frontend/src/features/auth/LoginForm.tsx @@ -16,7 +16,7 @@ const schema = z.object({ type FormValues = z.infer -export function LoginForm({ onSuccess }: { onSuccess: () => void }) { +export function LoginForm({ onSuccess }: Readonly<{ onSuccess: () => void }>) { const { t } = useTranslation() const { register: registerField, diff --git a/frontend/src/features/auth/RegisterForm.tsx b/frontend/src/features/auth/RegisterForm.tsx index 817df16..b2c9ceb 100644 --- a/frontend/src/features/auth/RegisterForm.tsx +++ b/frontend/src/features/auth/RegisterForm.tsx @@ -16,7 +16,7 @@ const schema = z.object({ type FormValues = z.infer -export function RegisterForm({ onSuccess }: { onSuccess: () => void }) { +export function RegisterForm({ onSuccess }: Readonly<{ onSuccess: () => void }>) { const { t } = useTranslation() const { register: registerField, diff --git a/frontend/src/features/streaming/AirPage.tsx b/frontend/src/features/streaming/AirPage.tsx index 95a71db..c50b50d 100644 --- a/frontend/src/features/streaming/AirPage.tsx +++ b/frontend/src/features/streaming/AirPage.tsx @@ -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 ( void -}) { +}>) { const { t } = useTranslation() const containerRef = useRef(null) const videoRef = useRef(null) diff --git a/frontend/src/features/streaming/PlayerOverlays.tsx b/frontend/src/features/streaming/PlayerOverlays.tsx index 65885fb..2f1442d 100644 --- a/frontend/src/features/streaming/PlayerOverlays.tsx +++ b/frontend/src/features/streaming/PlayerOverlays.tsx @@ -19,11 +19,11 @@ export function ChannelLogo({ imageId, corner, opacity, -}: { +}: Readonly<{ imageId: string corner: LogoCorner opacity: number -}) { +}>) { return ( ) { const { t } = useTranslation() return ( @@ -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 (
diff --git a/frontend/src/shared/ui/ToastProvider.tsx b/frontend/src/shared/ui/ToastProvider.tsx index cefdbf9..8bdb4e2 100644 --- a/frontend/src/shared/ui/ToastProvider.tsx +++ b/frontend/src/shared/ui/ToastProvider.tsx @@ -8,7 +8,7 @@ import { let nextId = 1 -export function ToastProvider({ children }: { children: ReactNode }) { +export function ToastProvider({ children }: Readonly<{ children: ReactNode }>) { const [toasts, setToasts] = useState([]) const push = useCallback((message: string, variant: ToastVariant) => { diff --git a/frontend/src/shared/ui/badge.tsx b/frontend/src/shared/ui/badge.tsx index 14d0f8e..0b953c2 100644 --- a/frontend/src/shared/ui/badge.tsx +++ b/frontend/src/shared/ui/badge.tsx @@ -18,6 +18,6 @@ const badgeVariants = cva( export type BadgeProps = HTMLAttributes & VariantProps -export function Badge({ className, variant, ...props }: BadgeProps) { +export function Badge({ className, variant, ...props }: Readonly) { return } diff --git a/frontend/src/shared/ui/hls-video.tsx b/frontend/src/shared/ui/hls-video.tsx index 8e77cfd..48e596d 100644 --- a/frontend/src/shared/ui/hls-video.tsx +++ b/frontend/src/shared/ui/hls-video.tsx @@ -7,7 +7,7 @@ import { cn } from '@/shared/lib/cn' * Мини-плеер HLS для админки: плейлист и сегменты лежат под admin-роутами (JWT), поэтому запросы * идут через 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(null) useEffect(() => { diff --git a/frontend/src/shared/ui/pager.tsx b/frontend/src/shared/ui/pager.tsx index 871ba7d..ac48f3b 100644 --- a/frontend/src/shared/ui/pager.tsx +++ b/frontend/src/shared/ui/pager.tsx @@ -6,11 +6,11 @@ export function Pager({ page, totalPages, onChange, -}: { +}: Readonly<{ page: number totalPages: number onChange: (page: number) => void -}) { +}>) { const { t } = useTranslation() if (totalPages <= 1) return null diff --git a/frontend/src/shared/ui/sortable.tsx b/frontend/src/shared/ui/sortable.tsx index 52ef439..aa13c99 100644 --- a/frontend/src/shared/ui/sortable.tsx +++ b/frontend/src/shared/ui/sortable.tsx @@ -9,13 +9,13 @@ export function SortHeader({ sort, onToggle, className, -}: { +}: Readonly<{ label: string sortKey: string sort: SortState onToggle: (key: string) => void className?: string -}) { +}>) { const active = sort.key === sortKey const direction = sort.desc ? 'descending' : 'ascending' let Icon = ChevronsUpDown diff --git a/frontend/src/shared/ui/toaster.tsx b/frontend/src/shared/ui/toaster.tsx index 2907ea1..fb65715 100644 --- a/frontend/src/shared/ui/toaster.tsx +++ b/frontend/src/shared/ui/toaster.tsx @@ -21,12 +21,12 @@ function ToastItem({ message, variant, onDismiss, -}: { +}: Readonly<{ id: number message: string variant: 'default' | 'success' | 'error' onDismiss: (id: number) => void -}) { +}>) { const { t } = useTranslation() useEffect(() => { diff --git a/frontend/src/theme/ThemeProvider.tsx b/frontend/src/theme/ThemeProvider.tsx index f69ab0b..effa6fb 100644 --- a/frontend/src/theme/ThemeProvider.tsx +++ b/frontend/src/theme/ThemeProvider.tsx @@ -13,7 +13,7 @@ function applyTheme(theme: Theme) { root.classList.toggle('dark', resolve(theme) === 'dark') } -export function ThemeProvider({ children }: { children: ReactNode }) { +export function ThemeProvider({ children }: Readonly<{ children: ReactNode }>) { const [theme, setThemeState] = useState( () => (localStorage.getItem(THEME_STORAGE_KEY) as Theme | null) ?? 'dark', )