Refactor ChannelDetail component: replace Card components with CollapsibleCard for improved UI organization and user experience. Introduce CollapsibleCard functionality to manage expandable content sections. Update AirPage to enhance time display formatting for better readability.

This commit is contained in:
Leonid Pershin
2026-07-25 10:10:36 +03:00
parent 9a17715d0f
commit 27571a4ab6
2 changed files with 52 additions and 47 deletions
@@ -1,8 +1,8 @@
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { Link } from '@tanstack/react-router'
import { useEffect, useState } from 'react'
import { type ReactNode, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { ChevronLeft, RefreshCw } from 'lucide-react'
import { ChevronDown, ChevronLeft, RefreshCw } from 'lucide-react'
import { HttpError } from '@/shared/api/client'
import type {
AdInsertion,
@@ -16,7 +16,7 @@ import type {
} from '@/shared/api/types'
import { Badge } from '@/shared/ui/badge'
import { Button } from '@/shared/ui/button'
import { Card, CardContent, CardHeader, CardTitle } from '@/shared/ui/card'
import { Card, CardContent, CardTitle } from '@/shared/ui/card'
import { Input } from '@/shared/ui/input'
import { Label } from '@/shared/ui/label'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/shared/ui/select'
@@ -118,11 +118,7 @@ export function ChannelDetail({ channelId }: { channelId: string }) {
<SettingsCard channel={channel} readyAssets={ready?.items ?? []} onSaved={invalidate} onError={onError} />
{/* Шоу канала */}
<Card>
<CardHeader>
<CardTitle>{t('admin.channels.shows')}</CardTitle>
</CardHeader>
<CardContent className="flex flex-col gap-3">
<CollapsibleCard title={t('admin.channels.shows')} contentClassName="flex flex-col gap-3">
<AddShowForm
channelId={channelId}
options={availableShows.map((s) => ({ id: s.id, name: s.name }))}
@@ -158,15 +154,10 @@ export function ChannelDetail({ channelId }: { channelId: string }) {
)}
</tbody>
</table>
</CardContent>
</Card>
</CollapsibleCard>
{/* Реклама */}
<Card>
<CardHeader>
<CardTitle>{t('admin.channels.ads')}</CardTitle>
</CardHeader>
<CardContent className="flex flex-col gap-3">
<CollapsibleCard title={t('admin.channels.ads')} contentClassName="flex flex-col gap-3">
<AddAdForm
channelId={channelId}
options={(ready?.items ?? [])
@@ -190,15 +181,10 @@ export function ChannelDetail({ channelId }: { channelId: string }) {
<li className="py-2 text-muted-foreground">{t('admin.channels.noAds')}</li>
)}
</ul>
</CardContent>
</Card>
</CollapsibleCard>
{/* Джинглы-отбивки (статичные заставки) */}
<Card>
<CardHeader>
<CardTitle>{t('admin.channels.jingles')}</CardTitle>
</CardHeader>
<CardContent className="flex flex-col gap-3">
<CollapsibleCard title={t('admin.channels.jingles')} contentClassName="flex flex-col gap-3">
<p className="text-sm text-muted-foreground">{t('admin.channels.jinglesHint')}</p>
<AddJingleForm
channelId={channelId}
@@ -223,15 +209,10 @@ export function ChannelDetail({ channelId }: { channelId: string }) {
<li className="py-2 text-muted-foreground">{t('admin.channels.noJingles')}</li>
)}
</ul>
</CardContent>
</Card>
</CollapsibleCard>
{/* Override'ы / марафоны */}
<Card>
<CardHeader>
<CardTitle>{t('admin.channels.overrides')}</CardTitle>
</CardHeader>
<CardContent className="flex flex-col gap-3">
<CollapsibleCard title={t('admin.channels.overrides')} contentClassName="flex flex-col gap-3">
<OverrideForm
channelId={channelId}
options={channel.shows.map((cs) => ({ id: cs.showId, name: cs.showName }))}
@@ -255,22 +236,47 @@ export function ChannelDetail({ channelId }: { channelId: string }) {
<li className="py-2 text-muted-foreground">{t('admin.channels.noOverrides')}</li>
)}
</ul>
</CardContent>
</Card>
</CollapsibleCard>
{/* Предпросмотр расписания */}
<Card>
<CardHeader>
<CardTitle>{t('admin.channels.schedule')}</CardTitle>
</CardHeader>
<CardContent>
<SchedulePreview entries={schedule ?? []} />
</CardContent>
</Card>
<CollapsibleCard title={t('admin.channels.schedule')}>
<SchedulePreview entries={schedule ?? []} />
</CollapsibleCard>
</div>
)
}
/** Карточка со сворачиваемым содержимым: клик по заголовку скрывает/раскрывает блок. */
function CollapsibleCard({
title,
defaultOpen = false,
contentClassName,
children,
}: {
title: string
defaultOpen?: boolean
contentClassName?: string
children: ReactNode
}) {
const [open, setOpen] = useState(defaultOpen)
return (
<Card>
<button
type="button"
onClick={() => setOpen((o) => !o)}
aria-expanded={open}
className="flex w-full items-center justify-between gap-2 p-6 text-left"
>
<CardTitle>{title}</CardTitle>
<ChevronDown
className={`h-5 w-5 shrink-0 text-muted-foreground transition-transform ${open ? 'rotate-180' : ''}`}
/>
</button>
{open && <CardContent className={contentClassName}>{children}</CardContent>}
</Card>
)
}
function SettingsCard({
channel,
readyAssets,
@@ -323,11 +329,11 @@ function SettingsCard({
})
return (
<Card>
<CardHeader>
<CardTitle>{t('admin.channels.settings')}</CardTitle>
</CardHeader>
<CardContent className="grid gap-4 sm:grid-cols-2">
<CollapsibleCard
title={t('admin.channels.settings')}
defaultOpen
contentClassName="grid gap-4 sm:grid-cols-2"
>
<div className="flex flex-col gap-1.5">
<Label>{t('admin.channels.name')}</Label>
<Input value={name} onChange={(e) => setName(e.target.value)} />
@@ -408,8 +414,7 @@ function SettingsCard({
{t('common.save')}
</Button>
</div>
</CardContent>
</Card>
</CollapsibleCard>
)
}
+1 -1
View File
@@ -179,7 +179,7 @@ export function AirPage() {
<ul className="divide-y divide-border">
{upcoming.slice(0, 6).map((block) => (
<li key={block.key} className="flex items-center gap-3 px-4 py-2 text-sm">
<span className="w-24 shrink-0 text-muted-foreground">
<span className="shrink-0 whitespace-nowrap tabular-nums text-muted-foreground">
{formatTime(block.startsAtUtc)} {formatTime(block.endsAtUtc)}
</span>
<span>{block.showName}</span>