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:
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user