39487-vm/frontend/src/pages/media-center.tsx
Flatlogic Bot 14079e71ec 1.3
2026-04-05 16:01:59 +00:00

732 lines
36 KiB
TypeScript

import React, { ReactElement, useEffect, useMemo, useState } from 'react';
import Head from 'next/head';
import axios from 'axios';
import {
mdiAccessPoint,
mdiBroadcast,
mdiMagnify,
mdiMovieOpenPlay,
mdiPlayCircleOutline,
mdiRadio,
mdiTelevisionClassic,
mdiViewGridPlus,
} from '@mdi/js';
import BaseButton from '../components/BaseButton';
import BaseIcon from '../components/BaseIcon';
import CardBox from '../components/CardBox';
import LayoutAuthenticated from '../layouts/Authenticated';
import LoadingSpinner from '../components/LoadingSpinner';
import MediaCenterUploadWidget from '../components/MediaCenterUploadWidget';
import SectionMain from '../components/SectionMain';
import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton';
import { getPageTitle } from '../config';
import { hasPermission } from '../helpers/userPermissions';
import { useAppSelector } from '../stores/hooks';
type FilterKind = 'all' | 'show' | 'episode' | 'live';
type LibraryItem = {
id: string;
kind: FilterKind;
title: string;
description: string;
badge: string;
status: string;
categoryName: string;
categoryId: string;
dateLabel: string;
meta: string;
href: string;
isFeatured: boolean;
sortDate: number;
};
const filterTabs: Array<{ label: string; value: FilterKind }> = [
{ label: 'All content', value: 'all' },
{ label: 'Shows', value: 'show' },
{ label: 'Episodes', value: 'episode' },
{ label: 'Live', value: 'live' },
];
const statusStyles: Record<string, string> = {
live: 'bg-red-500/15 text-red-700 dark:text-red-300 border border-red-500/20',
scheduled: 'bg-amber-500/15 text-amber-700 dark:text-amber-300 border border-amber-500/20',
published: 'bg-emerald-500/15 text-emerald-700 dark:text-emerald-300 border border-emerald-500/20',
draft: 'bg-slate-500/10 text-slate-700 dark:text-slate-300 border border-slate-500/20',
offline: 'bg-slate-500/10 text-slate-700 dark:text-slate-300 border border-slate-500/20',
archived: 'bg-slate-500/10 text-slate-700 dark:text-slate-300 border border-slate-500/20',
};
const cardAccent: Record<FilterKind, string> = {
all: 'from-sky-500/25 to-violet-500/20',
show: 'from-sky-500/25 to-violet-500/20',
episode: 'from-fuchsia-500/25 to-cyan-500/20',
live: 'from-rose-500/25 to-orange-500/20',
};
function getStatusClass(status?: string) {
return statusStyles[(status || '').toLowerCase()] || 'bg-slate-500/10 text-slate-700 dark:text-slate-300 border border-slate-500/20';
}
function formatDateLabel(value?: string | Date | null) {
if (!value) return 'No schedule set';
const date = new Date(value);
if (Number.isNaN(date.getTime())) return 'No schedule set';
return new Intl.DateTimeFormat('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
hour: 'numeric',
minute: '2-digit',
}).format(date);
}
function getTimestamp(value?: string | Date | null) {
if (!value) return 0;
const date = new Date(value);
if (Number.isNaN(date.getTime())) return 0;
return date.getTime();
}
function toYouTubeEmbed(url?: string) {
if (!url) return '';
try {
const parsed = new URL(url);
if (parsed.hostname.includes('youtu.be')) {
const id = parsed.pathname.replace('/', '');
return id ? `https://www.youtube.com/embed/${id}` : '';
}
if (parsed.hostname.includes('youtube.com')) {
if (parsed.pathname.includes('/embed/')) return url;
const id = parsed.searchParams.get('v');
return id ? `https://www.youtube.com/embed/${id}` : '';
}
} catch (error) {
return '';
}
return '';
}
function isDirectMedia(url?: string, type?: string) {
if (!url) return false;
const lowered = url.toLowerCase();
if (type === 'audio') {
return lowered.endsWith('.mp3') || lowered.endsWith('.aac') || lowered.endsWith('.wav') || lowered.endsWith('.ogg');
}
return (
lowered.endsWith('.mp4') ||
lowered.endsWith('.webm') ||
lowered.endsWith('.mov') ||
lowered.includes('.m3u8')
);
}
const MediaCenterPage = () => {
const { currentUser } = useAppSelector((state) => state.auth);
const [loading, setLoading] = useState(true);
const [errorMessage, setErrorMessage] = useState('');
const [shows, setShows] = useState<any[]>([]);
const [episodes, setEpisodes] = useState<any[]>([]);
const [liveStreams, setLiveStreams] = useState<any[]>([]);
const [categories, setCategories] = useState<any[]>([]);
const [searchTerm, setSearchTerm] = useState('');
const [selectedKind, setSelectedKind] = useState<FilterKind>('all');
const [selectedCategory, setSelectedCategory] = useState('all');
const canReadShows = currentUser && hasPermission(currentUser, 'READ_SHOWS');
const canReadEpisodes = currentUser && hasPermission(currentUser, 'READ_EPISODES');
const canReadLiveStreams = currentUser && hasPermission(currentUser, 'READ_LIVE_STREAMS');
const canReadCategories = currentUser && hasPermission(currentUser, 'READ_CATEGORIES');
const canCreateShows = currentUser && hasPermission(currentUser, 'CREATE_SHOWS');
const canCreateEpisodes = currentUser && hasPermission(currentUser, 'CREATE_EPISODES');
const canCreateLiveStreams = currentUser && hasPermission(currentUser, 'CREATE_LIVE_STREAMS');
useEffect(() => {
if (!currentUser) return;
let isActive = true;
const loadContent = async () => {
setLoading(true);
setErrorMessage('');
try {
const requests: Promise<{ key: string; rows: any[] }>[] = [];
if (canReadCategories) {
requests.push(
axios.get('categories?page=0&limit=50').then((response) => ({
key: 'categories',
rows: response.data.rows || [],
})),
);
}
if (canReadShows) {
requests.push(
axios.get('shows?page=0&limit=24').then((response) => ({
key: 'shows',
rows: response.data.rows || [],
})),
);
}
if (canReadEpisodes) {
requests.push(
axios.get('episodes?page=0&limit=36').then((response) => ({
key: 'episodes',
rows: response.data.rows || [],
})),
);
}
if (canReadLiveStreams) {
requests.push(
axios.get('live_streams?page=0&limit=18').then((response) => ({
key: 'liveStreams',
rows: response.data.rows || [],
})),
);
}
const results = await Promise.all(requests);
if (!isActive) return;
const mapped = results.reduce<Record<string, any[]>>((accumulator, item) => {
accumulator[item.key] = item.rows;
return accumulator;
}, {});
setCategories(mapped.categories || []);
setShows(mapped.shows || []);
setEpisodes(mapped.episodes || []);
setLiveStreams(mapped.liveStreams || []);
} catch (error) {
console.error('Failed to load media center data:', error);
if (isActive) {
setErrorMessage('We could not load the media library right now. Try refreshing the page in a moment.');
}
} finally {
if (isActive) {
setLoading(false);
}
}
};
loadContent();
return () => {
isActive = false;
};
}, [canReadCategories, canReadEpisodes, canReadLiveStreams, canReadShows, currentUser]);
const featuredLive = useMemo(() => {
if (!liveStreams.length) return null;
return (
liveStreams.find((item) => item.status === 'live') ||
liveStreams.find((item) => item.is_featured) ||
liveStreams[0]
);
}, [liveStreams]);
const libraryItems = useMemo<LibraryItem[]>(() => {
const showItems: LibraryItem[] = shows.map((item) => ({
id: item.id,
kind: 'show',
title: item.title || 'Untitled show',
description: item.summary || 'No summary yet for this show.',
badge: item.show_type ? String(item.show_type).replace(/_/g, ' ') : 'Show',
status: item.status || 'draft',
categoryName: item.category?.name || 'Uncategorized',
categoryId: item.category?.id || '',
dateLabel: item.release_year ? `Released in ${item.release_year}` : 'Release year not set',
meta: item.owner?.firstName ? `Owner: ${item.owner.firstName}` : 'Program shell',
href: `/shows/shows-view/?id=${item.id}`,
isFeatured: Boolean(item.is_featured),
sortDate: item.release_year ? Number(item.release_year) : 0,
}));
const episodeItems: LibraryItem[] = episodes.map((item) => ({
id: item.id,
kind: 'episode',
title: item.title || 'Untitled episode',
description: item.description || 'No description yet for this episode.',
badge:
item.season_number && item.episode_number
? `S${item.season_number} • E${item.episode_number}`
: 'Episode',
status: item.status || 'draft',
categoryName: item.show?.title || 'Standalone episode',
categoryId: '',
dateLabel: formatDateLabel(item.published_at || item.scheduled_at),
meta: item.duration_seconds ? `${Math.round(item.duration_seconds / 60)} min runtime` : 'Runtime not set',
href: `/episodes/episodes-view/?id=${item.id}`,
isFeatured: Boolean(item.is_featured),
sortDate: getTimestamp(item.published_at || item.scheduled_at),
}));
const liveItems: LibraryItem[] = liveStreams.map((item) => ({
id: item.id,
kind: 'live',
title: item.title || 'Untitled stream',
description: item.description || 'No stream description yet.',
badge: item.stream_type === 'audio' ? 'Audio stream' : 'Video stream',
status: item.status || 'offline',
categoryName: item.category?.name || 'Live programming',
categoryId: item.category?.id || '',
dateLabel: formatDateLabel(item.starts_at),
meta: item.host?.firstName ? `Host: ${item.host.firstName}` : 'Host not assigned',
href: `/live_streams/live_streams-view/?id=${item.id}`,
isFeatured: Boolean(item.is_featured),
sortDate: getTimestamp(item.starts_at || item.ends_at),
}));
const allItems = [...showItems, ...episodeItems, ...liveItems];
const query = searchTerm.trim().toLowerCase();
return allItems
.filter((item) => {
const matchesKind = selectedKind === 'all' || item.kind === selectedKind;
const matchesCategory = selectedCategory === 'all' || item.categoryId === selectedCategory;
const haystack = [item.title, item.description, item.categoryName, item.badge, item.meta]
.join(' ')
.toLowerCase();
const matchesQuery = !query || haystack.includes(query);
return matchesKind && matchesCategory && matchesQuery;
})
.sort((first, second) => {
if (first.isFeatured !== second.isFeatured) return Number(second.isFeatured) - Number(first.isFeatured);
return second.sortDate - first.sortDate;
});
}, [episodes, liveStreams, searchTerm, selectedCategory, selectedKind, shows]);
const upcomingLive = useMemo(() => {
return [...liveStreams]
.filter((item) => item.status === 'live' || item.status === 'scheduled')
.sort((first, second) => getTimestamp(first.starts_at) - getTimestamp(second.starts_at))
.slice(0, 4);
}, [liveStreams]);
const recentEpisodes = useMemo(() => {
return [...episodes]
.sort((first, second) => getTimestamp(second.published_at || second.scheduled_at) - getTimestamp(first.published_at || first.scheduled_at))
.slice(0, 4);
}, [episodes]);
const totals = {
shows: shows.length,
episodes: episodes.length,
liveStreams: liveStreams.length,
featured: [...shows, ...episodes, ...liveStreams].filter((item) => Boolean(item.is_featured)).length,
};
const playerEmbed = toYouTubeEmbed(featuredLive?.stream_url);
const canUseDirectPlayer = isDirectMedia(featuredLive?.stream_url, featuredLive?.stream_type);
return (
<>
<Head>
<title>{getPageTitle('Media center')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiBroadcast} title='Media center' main>
<div className='flex flex-wrap gap-2'>
{canCreateShows && <BaseButton href='/shows/shows-new' color='info' label='New show' />}
{canCreateEpisodes && <BaseButton href='/episodes/episodes-new' color='info' outline label='New episode' />}
{canCreateLiveStreams && <BaseButton href='/live_streams/live_streams-new' color='info' outline label='New live stream' />}
</div>
</SectionTitleLineWithButton>
<CardBox className='mb-6 overflow-hidden border-0 bg-transparent shadow-none'>
<div className='grid gap-6 rounded-[30px] bg-[linear-gradient(135deg,_#082032,_#0f172a_50%,_#1e293b)] p-6 text-white lg:grid-cols-[1.1fr_0.9fr] lg:p-8'>
<div>
<div className='inline-flex items-center gap-2 rounded-full border border-cyan-300/20 bg-cyan-300/10 px-4 py-2 text-sm text-cyan-100'>
<span className='h-2 w-2 rounded-full bg-emerald-400' />
Aliyo Momot editorial workflow
</div>
<h2 className='mt-4 text-3xl font-semibold tracking-tight'>Operate the broadcast from one workspace.</h2>
<p className='mt-4 max-w-2xl text-sm leading-7 text-slate-200/90'>
Browse live streams, published episodes, and show shells without hopping across multiple CRUD screens. When you need to edit or create content, jump directly into the existing admin forms.
</p>
<div className='mt-8 grid gap-4 sm:grid-cols-2 xl:grid-cols-4'>
<div className='rounded-3xl border border-white/10 bg-white/5 p-4'>
<p className='text-xs uppercase tracking-[0.24em] text-slate-300'>Shows</p>
<p className='mt-3 text-3xl font-semibold'>{totals.shows}</p>
</div>
<div className='rounded-3xl border border-white/10 bg-white/5 p-4'>
<p className='text-xs uppercase tracking-[0.24em] text-slate-300'>Episodes</p>
<p className='mt-3 text-3xl font-semibold'>{totals.episodes}</p>
</div>
<div className='rounded-3xl border border-white/10 bg-white/5 p-4'>
<p className='text-xs uppercase tracking-[0.24em] text-slate-300'>Live streams</p>
<p className='mt-3 text-3xl font-semibold'>{totals.liveStreams}</p>
</div>
<div className='rounded-3xl border border-white/10 bg-white/5 p-4'>
<p className='text-xs uppercase tracking-[0.24em] text-slate-300'>Featured items</p>
<p className='mt-3 text-3xl font-semibold'>{totals.featured}</p>
</div>
</div>
</div>
<div className='rounded-[28px] border border-white/10 bg-slate-950/30 p-5'>
<div className='flex items-center justify-between gap-4'>
<div>
<p className='text-xs uppercase tracking-[0.24em] text-cyan-100/70'>Next action</p>
<h3 className='mt-2 text-xl font-semibold'>Programming checklist</h3>
</div>
<BaseIcon path={mdiViewGridPlus} size={34} className='text-cyan-200' />
</div>
<div className='mt-6 space-y-3 text-sm text-slate-200'>
<div className='rounded-2xl border border-white/10 bg-white/5 p-4'>Review the featured live stream and make sure the link is playable.</div>
<div className='rounded-2xl border border-white/10 bg-white/5 p-4'>Search the library below, then open details pages for final polish.</div>
<div className='rounded-2xl border border-white/10 bg-white/5 p-4'>Use the quick actions to create the next show, episode, or stream.</div>
</div>
</div>
</div>
</CardBox>
<MediaCenterUploadWidget className='mb-6' />
{loading && <LoadingSpinner />}
{!loading && errorMessage && (
<CardBox className='mb-6 border border-red-200 bg-red-50 dark:border-red-900/40 dark:bg-red-950/20'>
<div className='space-y-3'>
<p className='text-lg font-semibold text-red-700 dark:text-red-300'>Unable to load the media center</p>
<p className='text-sm text-red-600 dark:text-red-200'>{errorMessage}</p>
</div>
</CardBox>
)}
{!loading && !errorMessage && (
<>
<div className='mb-6 grid gap-6 xl:grid-cols-[1.35fr_0.65fr]'>
<CardBox className='border border-slate-200/70 bg-white dark:border-slate-800'>
<div className='flex flex-col gap-5'>
<div className='flex flex-col gap-4 md:flex-row md:items-center md:justify-between'>
<div>
<p className='text-xs uppercase tracking-[0.24em] text-slate-500'>Discovery tools</p>
<h3 className='mt-2 text-2xl font-semibold text-slate-900 dark:text-white'>Search the catalog</h3>
</div>
<div className='flex flex-wrap gap-2'>
{filterTabs.map((tab) => {
const active = selectedKind === tab.value;
return (
<button
key={tab.value}
type='button'
onClick={() => setSelectedKind(tab.value)}
className={`rounded-full px-4 py-2 text-sm font-medium transition ${
active
? 'bg-slate-900 text-white dark:bg-white dark:text-slate-950'
: 'bg-slate-100 text-slate-600 hover:bg-slate-200 dark:bg-slate-800 dark:text-slate-200 dark:hover:bg-slate-700'
}`}
>
{tab.label}
</button>
);
})}
</div>
</div>
<div className='grid gap-4 lg:grid-cols-[minmax(0,1fr)_240px]'>
<label className='flex items-center gap-3 rounded-2xl border border-slate-200 bg-slate-50 px-4 py-3 text-slate-500 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-400'>
<BaseIcon path={mdiMagnify} size={22} />
<input
value={searchTerm}
onChange={(event) => setSearchTerm(event.target.value)}
placeholder='Search by title, show, category, or status'
className='w-full bg-transparent text-sm text-slate-900 outline-none placeholder:text-slate-400 dark:text-white'
/>
</label>
<label className='rounded-2xl border border-slate-200 bg-slate-50 px-4 py-3 text-sm text-slate-500 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-300'>
<span className='mb-2 block text-xs uppercase tracking-[0.22em]'>Category</span>
<select
value={selectedCategory}
onChange={(event) => setSelectedCategory(event.target.value)}
className='w-full bg-transparent text-sm text-slate-900 outline-none dark:text-white'
>
<option value='all'>All categories</option>
{categories.map((category) => (
<option key={category.id} value={category.id}>
{category.name || 'Unnamed category'}
</option>
))}
</select>
</label>
</div>
<p className='text-sm text-slate-500 dark:text-slate-300'>
{libraryItems.length} result{libraryItems.length === 1 ? '' : 's'} found. Use the cards below to open detail pages or jump into list screens.
</p>
</div>
</CardBox>
<CardBox className='border border-slate-200/70 bg-white dark:border-slate-800'>
<div className='space-y-4'>
<div>
<p className='text-xs uppercase tracking-[0.24em] text-slate-500'>Quick navigation</p>
<h3 className='mt-2 text-2xl font-semibold text-slate-900 dark:text-white'>Admin shortcuts</h3>
</div>
<div className='grid gap-3'>
{canReadShows && <BaseButton href='/shows/shows-list' color='info' label='Open shows library' />}
{canReadEpisodes && <BaseButton href='/episodes/episodes-list' color='info' outline label='Open episodes library' />}
{canReadLiveStreams && <BaseButton href='/live_streams/live_streams-list' color='info' outline label='Open live streams' />}
</div>
</div>
</CardBox>
</div>
<div className='mb-6 grid gap-6 xl:grid-cols-[1.1fr_0.9fr]'>
<CardBox className='overflow-hidden border border-slate-200/70 bg-white dark:border-slate-800'>
<div className='flex flex-col gap-5'>
<div className='flex items-center justify-between gap-4'>
<div>
<p className='text-xs uppercase tracking-[0.24em] text-slate-500'>Featured live booth</p>
<h3 className='mt-2 text-2xl font-semibold text-slate-900 dark:text-white'>Preview the current stream</h3>
</div>
<span className={`rounded-full px-3 py-1 text-xs font-medium ${getStatusClass(featuredLive?.status)}`}>
{featuredLive?.status || 'No live stream'}
</span>
</div>
{featuredLive ? (
<>
<div className='rounded-[26px] bg-[linear-gradient(135deg,_rgba(14,165,233,0.14),_rgba(15,23,42,0.06),_rgba(168,85,247,0.12))] p-4 dark:bg-[linear-gradient(135deg,_rgba(14,165,233,0.16),_rgba(2,6,23,0.95),_rgba(168,85,247,0.18))]'>
{playerEmbed ? (
<div className='aspect-video overflow-hidden rounded-[22px] border border-slate-200 bg-slate-950 dark:border-slate-800'>
<iframe
src={playerEmbed}
title={featuredLive.title}
allow='accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture'
allowFullScreen
className='h-full w-full'
/>
</div>
) : featuredLive.stream_type === 'audio' && canUseDirectPlayer ? (
<div className='rounded-[22px] border border-slate-200 bg-slate-950 p-5 dark:border-slate-800'>
<div className='flex items-center gap-3 text-white'>
<BaseIcon path={mdiRadio} size={28} className='text-cyan-300' />
<div>
<p className='text-sm uppercase tracking-[0.24em] text-slate-400'>Audio preview</p>
<p className='text-lg font-semibold text-white'>{featuredLive.title}</p>
</div>
</div>
<audio controls className='mt-5 w-full'>
<source src={featuredLive.stream_url} />
Your browser does not support audio playback.
</audio>
</div>
) : featuredLive.stream_type === 'video' && canUseDirectPlayer ? (
<div className='aspect-video overflow-hidden rounded-[22px] border border-slate-200 bg-slate-950 dark:border-slate-800'>
<video controls className='h-full w-full bg-black'>
<source src={featuredLive.stream_url} />
Your browser does not support video playback.
</video>
</div>
) : (
<div className='flex aspect-video flex-col items-center justify-center gap-4 rounded-[22px] border border-dashed border-slate-300 bg-slate-50 text-center dark:border-slate-700 dark:bg-slate-900/70'>
<BaseIcon path={mdiPlayCircleOutline} size={50} className='text-cyan-500' />
<div className='max-w-md space-y-2 px-5'>
<p className='text-lg font-semibold text-slate-900 dark:text-white'>Preview not embedded yet</p>
<p className='text-sm leading-6 text-slate-500 dark:text-slate-300'>
This stream uses a custom URL. Open the stream directly or update the link to a playable media source or YouTube embed URL.
</p>
</div>
{featuredLive.stream_url && (
<a
href={featuredLive.stream_url}
target='_blank'
rel='noreferrer'
className='rounded-full bg-slate-900 px-4 py-2 text-sm font-medium text-white transition hover:bg-slate-700 dark:bg-white dark:text-slate-950 dark:hover:bg-slate-200'
>
Open stream URL
</a>
)}
</div>
)}
</div>
<div className='grid gap-4 md:grid-cols-[1fr_auto] md:items-end'>
<div>
<h4 className='text-xl font-semibold text-slate-900 dark:text-white'>{featuredLive.title}</h4>
<p className='mt-2 text-sm leading-7 text-slate-500 dark:text-slate-300'>{featuredLive.description || 'No stream description yet.'}</p>
<div className='mt-4 flex flex-wrap gap-2'>
<span className='rounded-full bg-slate-100 px-3 py-1 text-xs text-slate-600 dark:bg-slate-800 dark:text-slate-200'>
{featuredLive.stream_type === 'audio' ? 'Audio stream' : 'Video stream'}
</span>
<span className='rounded-full bg-slate-100 px-3 py-1 text-xs text-slate-600 dark:bg-slate-800 dark:text-slate-200'>
{featuredLive.category?.name || 'Live programming'}
</span>
<span className='rounded-full bg-slate-100 px-3 py-1 text-xs text-slate-600 dark:bg-slate-800 dark:text-slate-200'>
{formatDateLabel(featuredLive.starts_at)}
</span>
</div>
</div>
<BaseButton href={`/live_streams/live_streams-view/?id=${featuredLive.id}`} color='info' label='Open stream details' />
</div>
</>
) : (
<div className='rounded-[24px] border border-dashed border-slate-300 bg-slate-50 p-6 text-center dark:border-slate-700 dark:bg-slate-900/70'>
<p className='text-lg font-semibold text-slate-900 dark:text-white'>No live streams yet</p>
<p className='mt-2 text-sm leading-6 text-slate-500 dark:text-slate-300'>Create a live stream to turn this area into an operational preview booth.</p>
{canCreateLiveStreams && <BaseButton href='/live_streams/live_streams-new' color='info' label='Create live stream' className='mt-4' />}
</div>
)}
</div>
</CardBox>
<div className='grid gap-6'>
<CardBox className='border border-slate-200/70 bg-white dark:border-slate-800'>
<div>
<div className='flex items-center gap-3'>
<BaseIcon path={mdiAccessPoint} size={24} className='text-cyan-500' />
<h3 className='text-xl font-semibold text-slate-900 dark:text-white'>Upcoming live slots</h3>
</div>
<div className='mt-5 space-y-3'>
{upcomingLive.length ? (
upcomingLive.map((item) => (
<a
key={item.id}
href={`/live_streams/live_streams-view/?id=${item.id}`}
className='block rounded-2xl border border-slate-200 p-4 transition hover:border-cyan-300 hover:bg-slate-50 dark:border-slate-800 dark:hover:bg-slate-900/70'
>
<div className='flex items-center justify-between gap-3'>
<p className='font-semibold text-slate-900 dark:text-white'>{item.title || 'Untitled stream'}</p>
<span className={`rounded-full px-3 py-1 text-xs font-medium ${getStatusClass(item.status)}`}>{item.status || 'offline'}</span>
</div>
<p className='mt-2 text-sm text-slate-500 dark:text-slate-300'>{formatDateLabel(item.starts_at)}</p>
</a>
))
) : (
<p className='text-sm text-slate-500 dark:text-slate-300'>No live schedule yet.</p>
)}
</div>
</div>
</CardBox>
<CardBox className='border border-slate-200/70 bg-white dark:border-slate-800'>
<div>
<div className='flex items-center gap-3'>
<BaseIcon path={mdiMovieOpenPlay} size={24} className='text-fuchsia-500' />
<h3 className='text-xl font-semibold text-slate-900 dark:text-white'>Recent episodes</h3>
</div>
<div className='mt-5 space-y-3'>
{recentEpisodes.length ? (
recentEpisodes.map((item) => (
<a
key={item.id}
href={`/episodes/episodes-view/?id=${item.id}`}
className='block rounded-2xl border border-slate-200 p-4 transition hover:border-fuchsia-300 hover:bg-slate-50 dark:border-slate-800 dark:hover:bg-slate-900/70'
>
<p className='font-semibold text-slate-900 dark:text-white'>{item.title || 'Untitled episode'}</p>
<p className='mt-2 text-sm text-slate-500 dark:text-slate-300'>{formatDateLabel(item.published_at || item.scheduled_at)}</p>
</a>
))
) : (
<p className='text-sm text-slate-500 dark:text-slate-300'>No episodes published yet.</p>
)}
</div>
</div>
</CardBox>
</div>
</div>
{libraryItems.length ? (
<div className='grid gap-5 md:grid-cols-2 xl:grid-cols-3'>
{libraryItems.map((item) => (
<CardBox
key={`${item.kind}-${item.id}`}
className='border border-slate-200/70 bg-white transition duration-300 hover:-translate-y-1 hover:shadow-xl dark:border-slate-800'
isHoverable
>
<div className='flex h-full flex-col'>
<div className={`rounded-[24px] bg-gradient-to-br ${cardAccent[item.kind]} p-4 dark:from-slate-900/90 dark:to-slate-800`}>
<div className='flex items-start justify-between gap-3'>
<div>
<span className='rounded-full bg-white/70 px-3 py-1 text-xs font-medium uppercase tracking-[0.2em] text-slate-700 dark:bg-white/10 dark:text-slate-200'>
{item.badge}
</span>
<h3 className='mt-4 text-xl font-semibold text-slate-900 dark:text-white'>{item.title}</h3>
</div>
<BaseIcon
path={item.kind === 'show' ? mdiTelevisionClassic : item.kind === 'episode' ? mdiMovieOpenPlay : mdiBroadcast}
size={30}
className='text-slate-700 dark:text-slate-100'
/>
</div>
</div>
<div className='flex flex-1 flex-col justify-between pt-5'>
<div>
<div className='flex flex-wrap gap-2'>
<span className={`rounded-full px-3 py-1 text-xs font-medium ${getStatusClass(item.status)}`}>{item.status}</span>
<span className='rounded-full bg-slate-100 px-3 py-1 text-xs text-slate-600 dark:bg-slate-800 dark:text-slate-200'>
{item.categoryName}
</span>
</div>
<p className='mt-4 text-sm leading-7 text-slate-500 dark:text-slate-300'>{item.description}</p>
</div>
<div className='mt-6 space-y-4'>
<div className='space-y-2 text-sm text-slate-500 dark:text-slate-300'>
<p>{item.dateLabel}</p>
<p>{item.meta}</p>
</div>
<div className='flex flex-wrap gap-3'>
<BaseButton href={item.href} color='info' label='View details' />
{item.kind === 'show' && canReadShows && <BaseButton href='/shows/shows-list' color='whiteDark' outline label='Open list' />}
{item.kind === 'episode' && canReadEpisodes && <BaseButton href='/episodes/episodes-list' color='whiteDark' outline label='Open list' />}
{item.kind === 'live' && canReadLiveStreams && <BaseButton href='/live_streams/live_streams-list' color='whiteDark' outline label='Open list' />}
</div>
</div>
</div>
</div>
</CardBox>
))}
</div>
) : (
<CardBox className='border border-dashed border-slate-300 bg-slate-50 dark:border-slate-700 dark:bg-slate-900/60'>
<div className='rounded-[24px] p-6 text-center'>
<p className='text-2xl font-semibold text-slate-900 dark:text-white'>No matching content yet</p>
<p className='mx-auto mt-3 max-w-2xl text-sm leading-7 text-slate-500 dark:text-slate-300'>
Adjust the search and filters, or create the first show, episode, or live stream to start shaping the Aliyo Momot media catalog.
</p>
<div className='mt-6 flex flex-wrap justify-center gap-3'>
{canCreateShows && <BaseButton href='/shows/shows-new' color='info' label='Create show' />}
{canCreateEpisodes && <BaseButton href='/episodes/episodes-new' color='info' outline label='Create episode' />}
{canCreateLiveStreams && <BaseButton href='/live_streams/live_streams-new' color='info' outline label='Create live stream' />}
</div>
</div>
</CardBox>
)}
</>
)}
</SectionMain>
</>
);
};
MediaCenterPage.getLayout = function getLayout(page: ReactElement) {
return <LayoutAuthenticated>{page}</LayoutAuthenticated>;
};
export default MediaCenterPage;