100 lines
3.2 KiB
TypeScript
100 lines
3.2 KiB
TypeScript
import { useNavigate } from 'react-router-dom'
|
|
import { motion } from 'framer-motion'
|
|
import { ListMusic } from '../lib/icons'
|
|
import { useLibraryItems } from '../hooks/use-jellyfin'
|
|
import PosterCard from '../components/ui/PosterCard'
|
|
import { usePosterGridClasses } from '../lib/density'
|
|
|
|
export default function PlaylistsPage() {
|
|
const navigate = useNavigate()
|
|
const gridCls = usePosterGridClasses()
|
|
|
|
const { data, isLoading } = useLibraryItems(undefined, {
|
|
includeItemTypes: ['Playlist'],
|
|
sortBy: ['SortName'],
|
|
sortOrder: ['Ascending'],
|
|
limit: 200,
|
|
})
|
|
|
|
const items = data?.Items || []
|
|
|
|
return (
|
|
<div className="px-7 pt-4 pb-12">
|
|
<div className="mb-7">
|
|
<div className="flex items-center gap-2 mb-1.5">
|
|
<span className="w-1 h-3.5 rounded-full bg-accent" />
|
|
<span className="text-[11px] font-semibold text-text-2 uppercase tracking-[0.14em]">Library</span>
|
|
</div>
|
|
<div className="flex items-baseline gap-3">
|
|
<h1 className="text-3xl md:text-4xl font-bold font-display text-text-1 tracking-tight">
|
|
Playlists
|
|
</h1>
|
|
{items.length > 0 && (
|
|
<span className="text-[12px] text-text-4 tabular-nums">
|
|
{items.length.toLocaleString()} {items.length === 1 ? 'playlist' : 'playlists'}
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{isLoading ? (
|
|
<SkeletonGrid />
|
|
) : items.length === 0 ? (
|
|
<EmptyState />
|
|
) : (
|
|
<div className={gridCls}>
|
|
{items.map((item, i) => (
|
|
<motion.div
|
|
key={item.Id}
|
|
initial={{ opacity: 0, y: 8 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ duration: 0.3, delay: Math.min(i * 0.012, 0.3), ease: [0.16, 1, 0.3, 1] }}
|
|
>
|
|
<PosterCard
|
|
item={item}
|
|
aspect="square"
|
|
priority={i < 12}
|
|
onClick={() => item.Id && navigate(`/item/${item.Id}`)}
|
|
/>
|
|
</motion.div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function EmptyState() {
|
|
return (
|
|
<div className="flex flex-col items-center justify-center min-h-[50vh] text-center">
|
|
<div className="relative w-16 h-16 mb-4">
|
|
<div className="absolute inset-0 rounded-full bg-accent/10 blur-xl" />
|
|
<div className="relative w-full h-full rounded-full bg-gradient-to-br from-elevated to-surface ring-1 ring-border grid place-items-center">
|
|
<ListMusic size={22} className="text-text-3" />
|
|
</div>
|
|
</div>
|
|
<p className="text-[15px] font-medium text-text-1 mb-1.5">No playlists yet</p>
|
|
<p className="text-[12px] text-text-4 max-w-sm">
|
|
Create a playlist on your Jellyfin server and it'll show up here.
|
|
</p>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function SkeletonGrid() {
|
|
const gridCls = usePosterGridClasses()
|
|
return (
|
|
<div className={gridCls}>
|
|
{Array.from({ length: 12 }).map((_, i) => (
|
|
<div key={i}>
|
|
<div className="skeleton aspect-square rounded-lg" />
|
|
<div className="mt-2 space-y-1">
|
|
<div className="skeleton h-3 w-3/4 rounded" />
|
|
<div className="skeleton h-2.5 w-1/2 rounded" />
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)
|
|
}
|