206 lines
8.0 KiB
TypeScript
206 lines
8.0 KiB
TypeScript
import React, { ReactElement, useEffect, useState } from 'react';
|
|
import Head from 'next/head';
|
|
import Link from 'next/link';
|
|
import { useRouter } from 'next/router';
|
|
import axios from 'axios';
|
|
import { mdiMagnify, mdiTelevisionClassic } from '@mdi/js';
|
|
import BaseIcon from '../../../components/BaseIcon';
|
|
import LoadingSpinner from '../../../components/LoadingSpinner';
|
|
import { getPageTitle } from '../../../config';
|
|
import { humanizeMediaKind } from '../../../helpers/publicMedia';
|
|
import LayoutGuest from '../../../layouts/Guest';
|
|
|
|
export default function PublicShowsPage() {
|
|
const router = useRouter();
|
|
const [loading, setLoading] = useState(true);
|
|
const [errorMessage, setErrorMessage] = useState('');
|
|
const [shows, setShows] = useState<any[]>([]);
|
|
const [categories, setCategories] = useState<any[]>([]);
|
|
const [searchTerm, setSearchTerm] = useState('');
|
|
const [selectedCategory, setSelectedCategory] = useState('');
|
|
|
|
useEffect(() => {
|
|
if (!router.isReady) return;
|
|
|
|
const categoryFromQuery = typeof router.query.categoryId === 'string' ? router.query.categoryId : '';
|
|
const queryFromUrl = typeof router.query.q === 'string' ? router.query.q : '';
|
|
|
|
setSelectedCategory(categoryFromQuery);
|
|
setSearchTerm(queryFromUrl);
|
|
}, [router.isReady, router.query.categoryId, router.query.q]);
|
|
|
|
useEffect(() => {
|
|
let isMounted = true;
|
|
|
|
const loadCategories = async () => {
|
|
try {
|
|
const response = await axios.get('/public-media/categories', { params: { limit: 24 } });
|
|
if (isMounted) {
|
|
setCategories(response.data?.rows || []);
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to load public categories:', error);
|
|
}
|
|
};
|
|
|
|
loadCategories();
|
|
|
|
return () => {
|
|
isMounted = false;
|
|
};
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
let isMounted = true;
|
|
|
|
const loadShows = async () => {
|
|
setLoading(true);
|
|
setErrorMessage('');
|
|
|
|
try {
|
|
const response = await axios.get('/public-media/shows', {
|
|
params: {
|
|
limit: 24,
|
|
q: searchTerm || undefined,
|
|
categoryId: selectedCategory || undefined,
|
|
},
|
|
});
|
|
|
|
if (isMounted) {
|
|
setShows(response.data?.rows || []);
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to load public shows:', error);
|
|
if (isMounted) {
|
|
setErrorMessage('We could not load shows right now.');
|
|
}
|
|
} finally {
|
|
if (isMounted) {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
};
|
|
|
|
loadShows();
|
|
|
|
return () => {
|
|
isMounted = false;
|
|
};
|
|
}, [searchTerm, selectedCategory]);
|
|
|
|
const applyCategory = (categoryId: string) => {
|
|
setSelectedCategory(categoryId);
|
|
void router.replace(
|
|
{
|
|
pathname: '/watch/shows',
|
|
query: {
|
|
...(searchTerm ? { q: searchTerm } : {}),
|
|
...(categoryId ? { categoryId } : {}),
|
|
},
|
|
},
|
|
undefined,
|
|
{ shallow: true },
|
|
);
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<Head>
|
|
<title>{getPageTitle('Shows')}</title>
|
|
<meta name='description' content='Browse all published Aliyo Momot shows by category and keyword.' />
|
|
</Head>
|
|
|
|
<main className='min-h-screen bg-[#050816] text-white'>
|
|
<section className='border-b border-white/5 bg-[linear-gradient(135deg,_#050816_0%,_#0f172a_55%,_#111827_100%)]'>
|
|
<div className='mx-auto max-w-7xl px-6 py-14 lg:px-10'>
|
|
<Link href='/watch' className='text-sm text-cyan-200 transition hover:text-white'>
|
|
← Back to watch hub
|
|
</Link>
|
|
<div className='mt-6 flex flex-col gap-6 lg:flex-row lg:items-end lg:justify-between'>
|
|
<div>
|
|
<p className='text-sm uppercase tracking-[0.28em] text-cyan-200/70'>Shows library</p>
|
|
<h1 className='mt-3 text-4xl font-semibold text-white sm:text-5xl'>Public shows, organized for discovery.</h1>
|
|
<p className='mt-4 max-w-3xl text-base leading-7 text-slate-300'>
|
|
Filter the published program lineup by keyword or category, then open a show to view its public episode archive.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className='mt-8 rounded-[28px] border border-white/10 bg-white/5 p-5'>
|
|
<label className='flex items-center gap-3 rounded-[20px] border border-white/10 bg-slate-950/60 px-4 py-3'>
|
|
<BaseIcon path={mdiMagnify} size={20} className='text-cyan-200' />
|
|
<input
|
|
value={searchTerm}
|
|
onChange={(event) => setSearchTerm(event.target.value)}
|
|
placeholder='Search show titles or summaries'
|
|
className='w-full bg-transparent text-sm text-white outline-none placeholder:text-slate-500'
|
|
/>
|
|
</label>
|
|
|
|
<div className='mt-4 flex flex-wrap gap-3'>
|
|
<button
|
|
type='button'
|
|
onClick={() => applyCategory('')}
|
|
className={`rounded-full px-4 py-2 text-sm transition ${selectedCategory ? 'border border-white/10 bg-white/5 text-slate-300 hover:text-white' : 'bg-white text-slate-950'}`}
|
|
>
|
|
All categories
|
|
</button>
|
|
{categories.map((category) => (
|
|
<button
|
|
key={category.id}
|
|
type='button'
|
|
onClick={() => applyCategory(category.id)}
|
|
className={`rounded-full px-4 py-2 text-sm transition ${selectedCategory === category.id ? 'bg-cyan-300 text-slate-950' : 'border border-white/10 bg-white/5 text-slate-300 hover:text-white'}`}
|
|
>
|
|
{category.name}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<section>
|
|
<div className='mx-auto max-w-7xl px-6 py-14 lg:px-10'>
|
|
{loading ? (
|
|
<LoadingSpinner />
|
|
) : errorMessage ? (
|
|
<div className='rounded-[24px] border border-rose-500/20 bg-rose-500/10 p-6 text-sm text-rose-100'>{errorMessage}</div>
|
|
) : shows.length ? (
|
|
<div className='grid gap-5 md:grid-cols-2 xl:grid-cols-3'>
|
|
{shows.map((show) => (
|
|
<Link key={show.id} href={`/watch/shows/${show.id}`} className='group rounded-[28px] border border-white/10 bg-white/5 p-6 transition hover:-translate-y-1 hover:border-cyan-300/35 hover:bg-white/[0.08]'>
|
|
<div className='flex items-start justify-between gap-4'>
|
|
<div className='rounded-2xl border border-white/10 bg-white/5 p-3'>
|
|
<BaseIcon path={mdiTelevisionClassic} size={28} className='text-cyan-200' />
|
|
</div>
|
|
<div className='rounded-full border border-white/10 bg-white/5 px-3 py-1 text-xs uppercase tracking-[0.22em] text-slate-300'>
|
|
{humanizeMediaKind(show.show_type)}
|
|
</div>
|
|
</div>
|
|
<h2 className='mt-6 text-2xl font-semibold text-white'>{show.title}</h2>
|
|
<p className='mt-3 line-clamp-3 text-sm leading-6 text-slate-300'>{show.summary || 'Published show ready for viewers.'}</p>
|
|
<div className='mt-6 flex flex-wrap gap-2 text-xs uppercase tracking-[0.18em] text-slate-400'>
|
|
<span>{show.category?.name || 'Uncategorized'}</span>
|
|
<span>•</span>
|
|
<span>{show.release_year || 'Current'}</span>
|
|
</div>
|
|
</Link>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<div className='rounded-[24px] border border-white/10 bg-white/5 p-8 text-center text-slate-300'>
|
|
No published shows matched your current filters.
|
|
</div>
|
|
)}
|
|
</div>
|
|
</section>
|
|
</main>
|
|
</>
|
|
);
|
|
}
|
|
|
|
PublicShowsPage.getLayout = function getLayout(page: ReactElement) {
|
|
return <LayoutGuest>{page}</LayoutGuest>;
|
|
};
|