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 { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { Link } from '@tanstack/react-router' import { Link } from '@tanstack/react-router'
import { useEffect, useState } from 'react' import { type ReactNode, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next' 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 { HttpError } from '@/shared/api/client'
import type { import type {
AdInsertion, AdInsertion,
@@ -16,7 +16,7 @@ import type {
} from '@/shared/api/types' } from '@/shared/api/types'
import { Badge } from '@/shared/ui/badge' import { Badge } from '@/shared/ui/badge'
import { Button } from '@/shared/ui/button' 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 { Input } from '@/shared/ui/input'
import { Label } from '@/shared/ui/label' import { Label } from '@/shared/ui/label'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/shared/ui/select' 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} /> <SettingsCard channel={channel} readyAssets={ready?.items ?? []} onSaved={invalidate} onError={onError} />
{/* Шоу канала */} {/* Шоу канала */}
<Card> <CollapsibleCard title={t('admin.channels.shows')} contentClassName="flex flex-col gap-3">
<CardHeader>
<CardTitle>{t('admin.channels.shows')}</CardTitle>
</CardHeader>
<CardContent className="flex flex-col gap-3">
<AddShowForm <AddShowForm
channelId={channelId} channelId={channelId}
options={availableShows.map((s) => ({ id: s.id, name: s.name }))} options={availableShows.map((s) => ({ id: s.id, name: s.name }))}
@@ -158,15 +154,10 @@ export function ChannelDetail({ channelId }: { channelId: string }) {
)} )}
</tbody> </tbody>
</table> </table>
</CardContent> </CollapsibleCard>
</Card>
{/* Реклама */} {/* Реклама */}
<Card> <CollapsibleCard title={t('admin.channels.ads')} contentClassName="flex flex-col gap-3">
<CardHeader>
<CardTitle>{t('admin.channels.ads')}</CardTitle>
</CardHeader>
<CardContent className="flex flex-col gap-3">
<AddAdForm <AddAdForm
channelId={channelId} channelId={channelId}
options={(ready?.items ?? []) 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> <li className="py-2 text-muted-foreground">{t('admin.channels.noAds')}</li>
)} )}
</ul> </ul>
</CardContent> </CollapsibleCard>
</Card>
{/* Джинглы-отбивки (статичные заставки) */} {/* Джинглы-отбивки (статичные заставки) */}
<Card> <CollapsibleCard title={t('admin.channels.jingles')} contentClassName="flex flex-col gap-3">
<CardHeader>
<CardTitle>{t('admin.channels.jingles')}</CardTitle>
</CardHeader>
<CardContent className="flex flex-col gap-3">
<p className="text-sm text-muted-foreground">{t('admin.channels.jinglesHint')}</p> <p className="text-sm text-muted-foreground">{t('admin.channels.jinglesHint')}</p>
<AddJingleForm <AddJingleForm
channelId={channelId} channelId={channelId}
@@ -223,15 +209,10 @@ export function ChannelDetail({ channelId }: { channelId: string }) {
<li className="py-2 text-muted-foreground">{t('admin.channels.noJingles')}</li> <li className="py-2 text-muted-foreground">{t('admin.channels.noJingles')}</li>
)} )}
</ul> </ul>
</CardContent> </CollapsibleCard>
</Card>
{/* Override'ы / марафоны */} {/* Override'ы / марафоны */}
<Card> <CollapsibleCard title={t('admin.channels.overrides')} contentClassName="flex flex-col gap-3">
<CardHeader>
<CardTitle>{t('admin.channels.overrides')}</CardTitle>
</CardHeader>
<CardContent className="flex flex-col gap-3">
<OverrideForm <OverrideForm
channelId={channelId} channelId={channelId}
options={channel.shows.map((cs) => ({ id: cs.showId, name: cs.showName }))} 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> <li className="py-2 text-muted-foreground">{t('admin.channels.noOverrides')}</li>
)} )}
</ul> </ul>
</CardContent> </CollapsibleCard>
</Card>
{/* Предпросмотр расписания */} {/* Предпросмотр расписания */}
<Card> <CollapsibleCard title={t('admin.channels.schedule')}>
<CardHeader> <SchedulePreview entries={schedule ?? []} />
<CardTitle>{t('admin.channels.schedule')}</CardTitle> </CollapsibleCard>
</CardHeader>
<CardContent>
<SchedulePreview entries={schedule ?? []} />
</CardContent>
</Card>
</div> </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({ function SettingsCard({
channel, channel,
readyAssets, readyAssets,
@@ -323,11 +329,11 @@ function SettingsCard({
}) })
return ( return (
<Card> <CollapsibleCard
<CardHeader> title={t('admin.channels.settings')}
<CardTitle>{t('admin.channels.settings')}</CardTitle> defaultOpen
</CardHeader> contentClassName="grid gap-4 sm:grid-cols-2"
<CardContent className="grid gap-4 sm:grid-cols-2"> >
<div className="flex flex-col gap-1.5"> <div className="flex flex-col gap-1.5">
<Label>{t('admin.channels.name')}</Label> <Label>{t('admin.channels.name')}</Label>
<Input value={name} onChange={(e) => setName(e.target.value)} /> <Input value={name} onChange={(e) => setName(e.target.value)} />
@@ -408,8 +414,7 @@ function SettingsCard({
{t('common.save')} {t('common.save')}
</Button> </Button>
</div> </div>
</CardContent> </CollapsibleCard>
</Card>
) )
} }
+1 -1
View File
@@ -179,7 +179,7 @@ export function AirPage() {
<ul className="divide-y divide-border"> <ul className="divide-y divide-border">
{upcoming.slice(0, 6).map((block) => ( {upcoming.slice(0, 6).map((block) => (
<li key={block.key} className="flex items-center gap-3 px-4 py-2 text-sm"> <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)} {formatTime(block.startsAtUtc)} {formatTime(block.endsAtUtc)}
</span> </span>
<span>{block.showName}</span> <span>{block.showName}</span>