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