Introduce pagination component and sticky views

Add a reusable pagination UI (src/components/ui/pagination.tsx) and integrate it into BrowseView and CastView. Replace previous simple prev/next handlers with handlePageChange and a getPaginationItems helper (ellipsis support), move filters/controls into sticky headers, make main content scrollable (browse-scroll-container / cast-scroll-container), and add sticky pagination bars. Also: fix footer to be fixed at bottom in App.tsx, increase bottom padding in DashboardView and DetailView, simplify MediaTable markup to render Table directly, and add /.windsurf to .gitignore. These changes improve UX for large result sets and keep controls accessible while scrolling.
This commit is contained in:
Lars Behrends
2026-04-26 15:43:41 +02:00
parent 4605b251be
commit b0cb8ca0a2
8 changed files with 672 additions and 405 deletions
+135 -137
View File
@@ -117,150 +117,148 @@ export default function MediaTable({
};
return (
<div className="w-full bg-[#0d0f14] rounded-lg border border-white/5 overflow-hidden">
<Table>
<TableHeader>
<TableRow className="border-b border-white/[0.03] hover:bg-transparent">
<TableHead
className="text-xs font-semibold text-gray-500 uppercase tracking-wider cursor-pointer hover:text-gray-300 transition-colors group w-[45%]"
onClick={() => handleSort('title')}
>
<div className="flex items-center">
Title <SortIcon field="title" />
</div>
</TableHead>
<TableHead
className="text-xs font-semibold text-gray-500 uppercase tracking-wider cursor-pointer hover:text-gray-300 transition-colors group w-[80px]"
onClick={() => handleSort('category')}
>
<div className="flex items-center">
Type <SortIcon field="category" />
</div>
</TableHead>
<TableHead
className="text-xs font-semibold text-gray-500 uppercase tracking-wider cursor-pointer hover:text-gray-300 transition-colors group w-[18%]"
onClick={() => handleSort('genre')}
>
<div className="flex items-center">
Genre <SortIcon field="genre" />
</div>
</TableHead>
<TableHead
className="text-xs font-semibold text-gray-500 uppercase tracking-wider cursor-pointer hover:text-gray-300 transition-colors group w-[70px] text-center"
onClick={() => handleSort('rating')}
>
<div className="flex items-center justify-center">
Rating <SortIcon field="rating" />
</div>
</TableHead>
<TableHead
className="text-xs font-semibold text-gray-500 uppercase tracking-wider cursor-pointer hover:text-gray-300 transition-colors group w-[60px] text-center"
onClick={() => handleSort('year')}
>
<div className="flex items-center justify-center">
Year <SortIcon field="year" />
</div>
</TableHead>
<TableHead
className="text-xs font-semibold text-gray-500 uppercase tracking-wider cursor-pointer hover:text-gray-300 transition-colors group w-[60px] text-right"
onClick={() => handleSort('plays')}
>
<div className="flex items-center justify-end">
Plays <SortIcon field="plays" />
</div>
</TableHead>
<TableHead className="w-[40px]"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{sortedMedia.map((media) => {
const categoryInfo = categoryConfig[media.category];
const CategoryIcon = categoryInfo?.icon;
const isFavorite = favoriteIds.has(media.id);
<Table className="w-full">
<TableHeader>
<TableRow className="border-b border-white/[0.03] hover:bg-transparent">
<TableHead
className="text-xs font-semibold text-gray-500 uppercase tracking-wider cursor-pointer hover:text-gray-300 transition-colors group w-[45%]"
onClick={() => handleSort('title')}
>
<div className="flex items-center">
Title <SortIcon field="title" />
</div>
</TableHead>
<TableHead
className="text-xs font-semibold text-gray-500 uppercase tracking-wider cursor-pointer hover:text-gray-300 transition-colors group w-[80px]"
onClick={() => handleSort('category')}
>
<div className="flex items-center">
Type <SortIcon field="category" />
</div>
</TableHead>
<TableHead
className="text-xs font-semibold text-gray-500 uppercase tracking-wider cursor-pointer hover:text-gray-300 transition-colors group w-[18%]"
onClick={() => handleSort('genre')}
>
<div className="flex items-center">
Genre <SortIcon field="genre" />
</div>
</TableHead>
<TableHead
className="text-xs font-semibold text-gray-500 uppercase tracking-wider cursor-pointer hover:text-gray-300 transition-colors group w-[70px] text-center"
onClick={() => handleSort('rating')}
>
<div className="flex items-center justify-center">
Rating <SortIcon field="rating" />
</div>
</TableHead>
<TableHead
className="text-xs font-semibold text-gray-500 uppercase tracking-wider cursor-pointer hover:text-gray-300 transition-colors group w-[60px] text-center"
onClick={() => handleSort('year')}
>
<div className="flex items-center justify-center">
Year <SortIcon field="year" />
</div>
</TableHead>
<TableHead
className="text-xs font-semibold text-gray-500 uppercase tracking-wider cursor-pointer hover:text-gray-300 transition-colors group w-[60px] text-right"
onClick={() => handleSort('plays')}
>
<div className="flex items-center justify-end">
Plays <SortIcon field="plays" />
</div>
</TableHead>
<TableHead className="w-[40px]"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{sortedMedia.map((media) => {
const categoryInfo = categoryConfig[media.category];
const CategoryIcon = categoryInfo?.icon;
const isFavorite = favoriteIds.has(media.id);
return (
<TableRow
key={media.id}
className="border-b border-white/[0.02] hover:bg-white/[0.02] transition-colors cursor-pointer group"
onClick={() => onMediaClick(media)}
>
{/* Title Cell with Poster */}
<TableCell className="py-2">
<div className="flex items-center gap-3">
<div className="relative w-10 h-14 rounded overflow-hidden shrink-0 bg-[#1a1d26]">
<img
src={media.poster}
alt={media.title}
className="w-full h-full object-cover"
referrerPolicy="no-referrer"
/>
</div>
<div className="min-w-0">
<div className="text-sm font-medium text-gray-200 truncate group-hover:text-[#e8466c] transition-colors">
{media.title}
</div>
return (
<TableRow
key={media.id}
className="border-b border-white/[0.02] hover:bg-white/[0.02] transition-colors cursor-pointer group"
onClick={() => onMediaClick(media)}
>
{/* Title Cell with Poster */}
<TableCell className="py-2">
<div className="flex items-center gap-3">
<div className="relative w-10 h-14 rounded overflow-hidden shrink-0 bg-[#1a1d26]">
<img
src={media.poster}
alt={media.title}
className="w-full h-full object-cover"
referrerPolicy="no-referrer"
/>
</div>
<div className="min-w-0">
<div className="text-sm font-medium text-gray-200 truncate group-hover:text-[#e8466c] transition-colors">
{media.title}
</div>
</div>
</TableCell>
</div>
</TableCell>
{/* Type Badge */}
<TableCell>
<span className={cn(
"inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-[10px] font-bold uppercase tracking-wide",
categoryInfo.bgColor,
categoryInfo.color
)}>
{CategoryIcon && <CategoryIcon size={9} />}
{categoryInfo.label}
{/* Type Badge */}
<TableCell>
<span className={cn(
"inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-[10px] font-bold uppercase tracking-wide",
categoryInfo.bgColor,
categoryInfo.color
)}>
{CategoryIcon && <CategoryIcon size={9} />}
{categoryInfo.label}
</span>
</TableCell>
{/* Genre */}
<TableCell>
<span className="text-sm text-gray-500 truncate block">
{media.genres?.join(', ') || '-'}
</span>
</TableCell>
{/* Rating */}
<TableCell className="text-center">
<div className="flex items-center justify-center gap-1">
<Star size={12} className="text-[#e8466c] fill-[#e8466c]" />
<span className="text-sm font-medium text-gray-300">
{media.rating?.toFixed(1) || '-'}
</span>
</TableCell>
</div>
</TableCell>
{/* Genre */}
<TableCell>
<span className="text-sm text-gray-500 truncate block">
{media.genres?.join(', ') || '-'}
</span>
</TableCell>
{/* Year */}
<TableCell className="text-center">
<span className="text-sm text-gray-400">{media.year}</span>
</TableCell>
{/* Rating */}
<TableCell className="text-center">
<div className="flex items-center justify-center gap-1">
<Star size={12} className="text-[#e8466c] fill-[#e8466c]" />
<span className="text-sm font-medium text-gray-300">
{media.rating?.toFixed(1) || '-'}
</span>
</div>
</TableCell>
{/* Plays */}
<TableCell className="text-right">
<span className="text-sm text-gray-400">{media.playCount || 0}</span>
</TableCell>
{/* Year */}
<TableCell className="text-center">
<span className="text-sm text-gray-400">{media.year}</span>
</TableCell>
{/* Plays */}
<TableCell className="text-right">
<span className="text-sm text-gray-400">{media.playCount || 0}</span>
</TableCell>
{/* Favorite */}
<TableCell>
<button
onClick={(e) => handleFavoriteClick(e, media)}
className={cn(
"p-1 rounded transition-colors",
isFavorite
? "text-[#e8466c]"
: "text-gray-600 hover:text-gray-500"
)}
>
<Heart size={14} className={cn(isFavorite && "fill-current")} />
</button>
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</div>
{/* Favorite */}
<TableCell>
<button
onClick={(e) => handleFavoriteClick(e, media)}
className={cn(
"p-1 rounded transition-colors",
isFavorite
? "text-[#e8466c]"
: "text-gray-600 hover:text-gray-500"
)}
>
<Heart size={14} className={cn(isFavorite && "fill-current")} />
</button>
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
);
}