From 419aff54fa3b4c90cff9cafc2b169c411d4b5f97 Mon Sep 17 00:00:00 2001 From: Leonid Pershin Date: Mon, 27 Jul 2026 00:25:11 +0300 Subject: [PATCH] Enhance EntryTraceDialog component by refactoring data display logic into dedicated summary functions for improved readability and maintainability. Update GridTab to streamline checkbox state management with a new toggle function. Refactor RulesCard to simplify window removal logic. Adjust CollectionsPanel, GenresPanel, GroupsPanel, RolesPanel, ShowsPanel, and UsersPanel to import sorting utilities from a centralized location, enhancing code organization. Update ThemeProvider to utilize a shared theme context for better consistency across the application. --- frontend/.oxlintrc.json | 12 +- .../channels/components/EntryTraceDialog.tsx | 93 +++++++------ .../admin/channels/components/GridTab.tsx | 11 +- .../admin/channels/components/RulesCard.tsx | 5 +- .../admin/collections/CollectionsPanel.tsx | 3 +- .../src/features/admin/genres/GenresPanel.tsx | 3 +- .../src/features/admin/groups/GroupsPanel.tsx | 3 +- .../admin/interstitials/BlockBuilder.tsx | 4 +- .../admin/media/ManualInboxDialog.tsx | 11 +- .../src/features/admin/media/MediaPanel.tsx | 14 +- .../src/features/admin/media/episode-parse.ts | 76 +++++----- frontend/src/features/admin/media/format.ts | 10 ++ .../src/features/admin/media/upload-store.ts | 131 ++++++++++-------- .../src/features/admin/roles/RolesPanel.tsx | 3 +- .../src/features/admin/shows/ShowDetail.tsx | 2 +- .../src/features/admin/shows/ShowsPanel.tsx | 3 +- .../src/features/admin/users/UsersPanel.tsx | 3 +- .../src/features/streaming/ChannelPlayer.tsx | 31 +++-- frontend/src/main.tsx | 2 +- frontend/src/routes/__root.tsx | 2 +- frontend/src/shared/lib/table-sort.ts | 37 +++++ frontend/src/shared/ui/ToastProvider.tsx | 29 ++++ frontend/src/shared/ui/sortable.tsx | 38 +---- frontend/src/shared/ui/toast-store.ts | 31 +++++ frontend/src/theme/ThemeProvider.tsx | 31 +---- frontend/src/theme/theme-context.ts | 18 +++ 26 files changed, 355 insertions(+), 251 deletions(-) create mode 100644 frontend/src/features/admin/media/format.ts create mode 100644 frontend/src/shared/lib/table-sort.ts create mode 100644 frontend/src/shared/ui/ToastProvider.tsx create mode 100644 frontend/src/shared/ui/toast-store.ts create mode 100644 frontend/src/theme/theme-context.ts diff --git a/frontend/.oxlintrc.json b/frontend/.oxlintrc.json index 6fa991d..1c24eee 100644 --- a/frontend/.oxlintrc.json +++ b/frontend/.oxlintrc.json @@ -4,5 +4,15 @@ "rules": { "react/rules-of-hooks": "error", "react/only-export-components": ["warn", { "allowConstantExport": true }] - } + }, + "overrides": [ + { + // File-based routing TanStack Router: файл роута обязан экспортировать `Route` рядом с + // компонентом страницы — правило тут неисполнимо в принципе. + "files": ["src/routes/**"], + "rules": { + "react/only-export-components": "off" + } + } + ] } diff --git a/frontend/src/features/admin/channels/components/EntryTraceDialog.tsx b/frontend/src/features/admin/channels/components/EntryTraceDialog.tsx index 8d5fd04..6d9b3e2 100644 --- a/frontend/src/features/admin/channels/components/EntryTraceDialog.tsx +++ b/frontend/src/features/admin/channels/components/EntryTraceDialog.tsx @@ -1,6 +1,7 @@ import { useQuery } from '@tanstack/react-query' import { qk } from '@/shared/api/query-keys' import { useTranslation } from 'react-i18next' +import type { EntryTraceDto } from '@/shared/api/types' import { Dialog, DialogContent, @@ -42,52 +43,11 @@ export function EntryTraceDialog({ {data && (
- - {data.layerName - ? `${data.layerName}${data.layerPriority !== null ? ` (${t('admin.channels.priority')} ${data.layerPriority})` : ''}` - : null} - - - {data.slotTitle - ? [ - data.slotTitle, - data.slotWeekday === null - ? t('admin.channels.everyDay') - : t(`admin.channels.weekdays.${data.slotWeekday}`), - data.slotTargetStart?.slice(0, 5), - data.slotDurationMinutes ? `${data.slotDurationMinutes} мин` : null, - data.driftMinutes !== 0 - ? t('admin.channels.traceDrift', { minutes: data.driftMinutes }) - : null, - data.snapped ? t('admin.channels.traceSnapped') : null, - ] - .filter(Boolean) - .join(' · ') - : null} - - - {data.groupName - ? `${data.groupName}${data.groupItemCount !== null ? ` (${data.groupItemCount})` : ''}` - : null} - + {layerSummary(data, t)} + {slotSummary(data, t)} + {groupSummary(data)} {data.collectionName} - - {data.strategy - ? [ - t(`admin.channels.strategies.${data.strategy}`), - data.cooldownDays - ? t('admin.channels.traceCooldown', { days: data.cooldownDays }) - : null, - data.candidatesAfterCooldown !== null - ? t('admin.channels.traceCandidates', { - count: data.candidatesAfterCooldown, - }) - : null, - ] - .filter(Boolean) - .join(' · ') - : null} - + {strategySummary(data, t)} {data.junctionName}
)} @@ -96,6 +56,49 @@ export function EntryTraceDialog({ ) } +type Translate = ReturnType['t'] + +/** Склейка непустых частей строки трейса; пусто — значит строка не заполнена (покажем «—»). */ +const joinParts = (parts: (string | null | undefined)[]) => parts.filter(Boolean).join(' · ') || null + +function layerSummary(data: EntryTraceDto, t: Translate) { + if (!data.layerName) return null + const priority = + data.layerPriority !== null ? ` (${t('admin.channels.priority')} ${data.layerPriority})` : '' + return `${data.layerName}${priority}` +} + +function slotSummary(data: EntryTraceDto, t: Translate) { + if (!data.slotTitle) return null + return joinParts([ + data.slotTitle, + data.slotWeekday === null + ? t('admin.channels.everyDay') + : t(`admin.channels.weekdays.${data.slotWeekday}`), + data.slotTargetStart?.slice(0, 5), + data.slotDurationMinutes ? `${data.slotDurationMinutes} мин` : null, + data.driftMinutes !== 0 ? t('admin.channels.traceDrift', { minutes: data.driftMinutes }) : null, + data.snapped ? t('admin.channels.traceSnapped') : null, + ]) +} + +function groupSummary(data: EntryTraceDto) { + if (!data.groupName) return null + const count = data.groupItemCount !== null ? ` (${data.groupItemCount})` : '' + return `${data.groupName}${count}` +} + +function strategySummary(data: EntryTraceDto, t: Translate) { + if (!data.strategy) return null + return joinParts([ + t(`admin.channels.strategies.${data.strategy}`), + data.cooldownDays ? t('admin.channels.traceCooldown', { days: data.cooldownDays }) : null, + data.candidatesAfterCooldown !== null + ? t('admin.channels.traceCandidates', { count: data.candidatesAfterCooldown }) + : null, + ]) +} + function Row({ label, children }: { label: string; children: React.ReactNode }) { return ( <> diff --git a/frontend/src/features/admin/channels/components/GridTab.tsx b/frontend/src/features/admin/channels/components/GridTab.tsx index 227ba00..0d9b378 100644 --- a/frontend/src/features/admin/channels/components/GridTab.tsx +++ b/frontend/src/features/admin/channels/components/GridTab.tsx @@ -55,6 +55,9 @@ export function GridTab({ const [copyTargets, setCopyTargets] = useState([]) const [copyFromChannel, setCopyFromChannel] = useState('') + const toggleCopyTarget = (day: number, checked: boolean) => + setCopyTargets((current) => (checked ? [...current, day] : current.filter((d) => d !== day))) + const { data: channels } = useQuery({ queryKey: qk.channels.all, queryFn: listChannels }) const addLayerMutation = useMutation({ @@ -318,13 +321,7 @@ export function GridTab({ - setCopyTargets((current) => - e.target.checked - ? [...current, day] - : current.filter((d) => d !== day), - ) - } + onChange={(e) => toggleCopyTarget(day, e.target.checked)} /> {t(`admin.channels.weekdays.${day}`)} diff --git a/frontend/src/features/admin/channels/components/RulesCard.tsx b/frontend/src/features/admin/channels/components/RulesCard.tsx index 71c579a..b029781 100644 --- a/frontend/src/features/admin/channels/components/RulesCard.tsx +++ b/frontend/src/features/admin/channels/components/RulesCard.tsx @@ -87,6 +87,9 @@ export function RulesCard({ onError, }) + const removeWindow = (key: string) => + setWindows((current) => current.filter((row) => row.key !== key)) + const patchWindow = (key: string, part: Partial) => setWindows((current) => current.map((row) => @@ -156,7 +159,7 @@ export function RulesCard({ diff --git a/frontend/src/features/admin/collections/CollectionsPanel.tsx b/frontend/src/features/admin/collections/CollectionsPanel.tsx index d1f2201..2d56f1c 100644 --- a/frontend/src/features/admin/collections/CollectionsPanel.tsx +++ b/frontend/src/features/admin/collections/CollectionsPanel.tsx @@ -7,7 +7,8 @@ import { qk } from '@/shared/api/query-keys' import { useApiError } from '@/shared/lib/use-api-error' import { Button } from '@/shared/ui/button' import { Input } from '@/shared/ui/input' -import { SortHeader, sortRows, useTableSort } from '@/shared/ui/sortable' +import { sortRows, useTableSort } from '@/shared/lib/table-sort' +import { SortHeader } from '@/shared/ui/sortable' import { createCollection, deleteCollection, listCollections } from './api' export function CollectionsPanel() { diff --git a/frontend/src/features/admin/genres/GenresPanel.tsx b/frontend/src/features/admin/genres/GenresPanel.tsx index e8202fa..eaa7fb1 100644 --- a/frontend/src/features/admin/genres/GenresPanel.tsx +++ b/frontend/src/features/admin/genres/GenresPanel.tsx @@ -19,7 +19,8 @@ import { } from '@/shared/ui/dialog' import { Input } from '@/shared/ui/input' import { Label } from '@/shared/ui/label' -import { SortHeader, sortRows, useTableSort } from '@/shared/ui/sortable' +import { sortRows, useTableSort } from '@/shared/lib/table-sort' +import { SortHeader } from '@/shared/ui/sortable' import { createGenre, deleteGenre, listGenres, updateGenre } from './api' const createSchema = z.object({ diff --git a/frontend/src/features/admin/groups/GroupsPanel.tsx b/frontend/src/features/admin/groups/GroupsPanel.tsx index 1a38f7f..208af53 100644 --- a/frontend/src/features/admin/groups/GroupsPanel.tsx +++ b/frontend/src/features/admin/groups/GroupsPanel.tsx @@ -7,7 +7,8 @@ import { useApiError } from '@/shared/lib/use-api-error' import { Badge } from '@/shared/ui/badge' import { Button } from '@/shared/ui/button' import { Input } from '@/shared/ui/input' -import { SortHeader, sortRows, useTableSort } from '@/shared/ui/sortable' +import { sortRows, useTableSort } from '@/shared/lib/table-sort' +import { SortHeader } from '@/shared/ui/sortable' import { createGroup, deleteGroup, listGroups } from './api' import { DurationLabel } from './DurationLabel' diff --git a/frontend/src/features/admin/interstitials/BlockBuilder.tsx b/frontend/src/features/admin/interstitials/BlockBuilder.tsx index 2e0ed35..b956d4d 100644 --- a/frontend/src/features/admin/interstitials/BlockBuilder.tsx +++ b/frontend/src/features/admin/interstitials/BlockBuilder.tsx @@ -51,6 +51,8 @@ export function BlockBuilder({ setItems((current) => [...current, item]) } + const removeAt = (index: number) => setItems((current) => current.filter((_, i) => i !== index)) + /** Перестановка внутри черновика — до сохранения всё живёт в состоянии, запросов нет. */ const reorder = (target: number) => { if (dragged === null || dragged === target) return @@ -110,7 +112,7 @@ export function BlockBuilder({ diff --git a/frontend/src/features/admin/media/ManualInboxDialog.tsx b/frontend/src/features/admin/media/ManualInboxDialog.tsx index 75db6bd..b3dbec3 100644 --- a/frontend/src/features/admin/media/ManualInboxDialog.tsx +++ b/frontend/src/features/admin/media/ManualInboxDialog.tsx @@ -188,6 +188,11 @@ export function ManualInboxDialog({ onClose }: { onClose: () => void }) { current.includes(path) ? current.filter((p) => p !== path) : [...current, path], ) + const toggleCollapsed = (folder: string) => + setCollapsed((current) => + current.includes(folder) ? current.filter((f) => f !== folder) : [...current, folder], + ) + const toggleFolder = (files: ManualInboxFileDto[]) => { const paths = files.filter((f) => !f.alreadyImported).map((f) => f.relativePath) const allSelected = paths.every((p) => selected.includes(p)) @@ -355,11 +360,7 @@ export function ManualInboxDialog({ onClose }: { onClose: () => void }) {