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:
@@ -7,3 +7,4 @@ coverage/
|
|||||||
.env*
|
.env*
|
||||||
!.env.example
|
!.env.example
|
||||||
/docs
|
/docs
|
||||||
|
/.windsurf
|
||||||
|
|||||||
+1
-1
@@ -544,7 +544,7 @@ function AppContent() {
|
|||||||
</LayoutGroup>
|
</LayoutGroup>
|
||||||
|
|
||||||
{/* Footer */}
|
{/* Footer */}
|
||||||
<footer className="py-6 px-6 border-t border-white/5 bg-[#0a0c10] mt-auto">
|
<footer className="fixed bottom-0 left-64 right-0 py-4 px-6 border-t border-white/5 bg-[#0a0c10] z-40">
|
||||||
<div className="max-w-[1920px] mx-auto flex flex-col md:flex-row items-center justify-between gap-4">
|
<div className="max-w-[1920px] mx-auto flex flex-col md:flex-row items-center justify-between gap-4">
|
||||||
<div className="flex items-center gap-2 text-sm font-medium text-gray-500">
|
<div className="flex items-center gap-2 text-sm font-medium text-gray-500">
|
||||||
<span>{mediaCounts.all} total</span>
|
<span>{mediaCounts.all} total</span>
|
||||||
|
|||||||
+113
-39
@@ -2,12 +2,21 @@ import { Media, MediaCategory, Staff } from '@/types';
|
|||||||
import MediaCard from './MediaCard';
|
import MediaCard from './MediaCard';
|
||||||
import MediaTable from './MediaTable';
|
import MediaTable from './MediaTable';
|
||||||
import MediaFilters from './filters/MediaFilters';
|
import MediaFilters from './filters/MediaFilters';
|
||||||
import { LayoutGrid, List, ChevronLeft, ChevronRight, User, Users } from 'lucide-react';
|
import { LayoutGrid, List, User, Users } from 'lucide-react';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import Loading from '@/components/ui/loading';
|
import Loading from '@/components/ui/loading';
|
||||||
import React, { useState, useMemo, useEffect } from 'react';
|
import React, { useState, useMemo, useEffect } from 'react';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
import {
|
||||||
|
Pagination,
|
||||||
|
PaginationContent,
|
||||||
|
PaginationEllipsis,
|
||||||
|
PaginationItem,
|
||||||
|
PaginationLink,
|
||||||
|
PaginationNext,
|
||||||
|
PaginationPrevious,
|
||||||
|
} from '@/components/ui/pagination';
|
||||||
|
|
||||||
interface BrowseViewProps {
|
interface BrowseViewProps {
|
||||||
mediaList: Media[];
|
mediaList: Media[];
|
||||||
@@ -110,14 +119,50 @@ export default function BrowseView({
|
|||||||
setSelectedSource(null);
|
setSelectedSource(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePrevPage = () => {
|
const handlePageChange = (page: number) => {
|
||||||
setCurrentPage((prev) => Math.max(prev - 1, 1));
|
setCurrentPage(page);
|
||||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
const scrollContainer = document.getElementById('browse-scroll-container');
|
||||||
|
if (scrollContainer) {
|
||||||
|
scrollContainer.scrollTo({ top: 0, behavior: 'smooth' });
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleNextPage = () => {
|
// Generate pagination items with ellipsis
|
||||||
setCurrentPage((prev) => Math.min(prev + 1, totalPages));
|
const getPaginationItems = () => {
|
||||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
const items: (number | string)[] = [];
|
||||||
|
const maxVisible = 5;
|
||||||
|
|
||||||
|
if (totalPages <= maxVisible) {
|
||||||
|
for (let i = 1; i <= totalPages; i++) {
|
||||||
|
items.push(i);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Always show first page
|
||||||
|
items.push(1);
|
||||||
|
|
||||||
|
if (currentPage > 3) {
|
||||||
|
items.push('ellipsis-start');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show pages around current
|
||||||
|
const start = Math.max(2, currentPage - 1);
|
||||||
|
const end = Math.min(totalPages - 1, currentPage + 1);
|
||||||
|
|
||||||
|
for (let i = start; i <= end; i++) {
|
||||||
|
items.push(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentPage < totalPages - 2) {
|
||||||
|
items.push('ellipsis-end');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always show last page
|
||||||
|
if (totalPages > 1) {
|
||||||
|
items.push(totalPages);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return items;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Calculate favorite IDs
|
// Calculate favorite IDs
|
||||||
@@ -136,9 +181,11 @@ export default function BrowseView({
|
|||||||
}, [searchResultsCast, itemsPerPage]);
|
}, [searchResultsCast, itemsPerPage]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="pt-6 pb-12 px-6 w-full mx-auto">
|
<div className="flex flex-col h-[calc(100vh-4rem-4rem)] w-full">
|
||||||
|
{/* Sticky Header - Filter + Results Summary + Count */}
|
||||||
|
<div className="px-6 pt-4 pb-4 bg-background border-b border-white/10 shrink-0 z-10">
|
||||||
{/* Filters Bar */}
|
{/* Filters Bar */}
|
||||||
<div className="flex flex-wrap items-center justify-between gap-4 mb-6">
|
<div className="flex flex-wrap items-center justify-between gap-4 mb-4">
|
||||||
<MediaFilters
|
<MediaFilters
|
||||||
mediaList={mediaList}
|
mediaList={mediaList}
|
||||||
activeCategory={activeCategory}
|
activeCategory={activeCategory}
|
||||||
@@ -233,12 +280,15 @@ export default function BrowseView({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Results Count */}
|
{/* Results Count */}
|
||||||
<div className="flex items-center justify-between mb-4">
|
<div className="flex items-center justify-between">
|
||||||
<p className="text-sm text-gray-500">
|
<p className="text-sm text-gray-500">
|
||||||
Showing {paginatedMedia.length} of {filteredMedia.length} results
|
Showing {paginatedMedia.length} of {filteredMedia.length} results
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Scrollable Content Area */}
|
||||||
|
<div id="browse-scroll-container" className="flex-1 overflow-y-auto px-6 pt-4 pb-20">
|
||||||
{/* Cast Search Results */}
|
{/* Cast Search Results */}
|
||||||
{hasSearchResults && hasCastResults && onCastClick && (
|
{hasSearchResults && hasCastResults && onCastClick && (
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
@@ -294,7 +344,7 @@ export default function BrowseView({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Content */}
|
{/* Content - inside scrollable area */}
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<Loading message="Loading media..." />
|
<Loading message="Loading media..." />
|
||||||
) : mediaList.length === 0 && !hasCastResults ? (
|
) : mediaList.length === 0 && !hasCastResults ? (
|
||||||
@@ -342,9 +392,13 @@ export default function BrowseView({
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Pagination Controls */}
|
{/* End of scrollable content area */}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Sticky Pagination Controls */}
|
||||||
{filteredMedia.length > 0 && (
|
{filteredMedia.length > 0 && (
|
||||||
<div className="mt-8 flex flex-col sm:flex-row items-center justify-between gap-6 border-t border-white/10 pt-6">
|
<div className="px-6 py-4 border-t border-white/10 bg-background shrink-0 z-10">
|
||||||
|
<div className="flex flex-col sm:flex-row items-center justify-between gap-4">
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<span className="text-sm text-gray-500 font-medium">Items per page:</span>
|
<span className="text-sm text-gray-500 font-medium">Items per page:</span>
|
||||||
<select
|
<select
|
||||||
@@ -361,34 +415,54 @@ export default function BrowseView({
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-6">
|
<Pagination>
|
||||||
<Button
|
<PaginationContent>
|
||||||
variant="outline"
|
<PaginationItem>
|
||||||
size="sm"
|
<PaginationPrevious
|
||||||
onClick={handlePrevPage}
|
onClick={() => handlePageChange(Math.max(currentPage - 1, 1))}
|
||||||
disabled={currentPage === 1}
|
className={cn(
|
||||||
className="gap-2 font-bold border-white/10 bg-transparent text-gray-300 hover:bg-white/5 hover:text-white disabled:opacity-50"
|
"border-white/10 bg-transparent text-gray-300 hover:bg-white/5 hover:text-white",
|
||||||
>
|
currentPage === 1 && "pointer-events-none opacity-50"
|
||||||
<ChevronLeft size={16} />
|
)}
|
||||||
Previous
|
/>
|
||||||
</Button>
|
</PaginationItem>
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
{getPaginationItems().map((item, index) => (
|
||||||
<span className="text-sm font-bold text-[#e8466c]">{currentPage}</span>
|
<React.Fragment key={index}>
|
||||||
<span className="text-sm text-gray-500 font-medium">of</span>
|
{item === 'ellipsis-start' || item === 'ellipsis-end' ? (
|
||||||
<span className="text-sm font-bold text-gray-300">{totalPages || 1}</span>
|
<PaginationItem>
|
||||||
</div>
|
<PaginationEllipsis />
|
||||||
|
</PaginationItem>
|
||||||
<Button
|
) : (
|
||||||
variant="outline"
|
<PaginationItem>
|
||||||
size="sm"
|
<PaginationLink
|
||||||
onClick={handleNextPage}
|
isActive={currentPage === item}
|
||||||
disabled={currentPage === totalPages || totalPages === 0}
|
onClick={() => handlePageChange(item as number)}
|
||||||
className="gap-2 font-bold border-white/10 bg-transparent text-gray-300 hover:bg-white/5 hover:text-white disabled:opacity-50"
|
className={cn(
|
||||||
|
"border-white/10",
|
||||||
|
currentPage === item
|
||||||
|
? "bg-[#e8466c]/20 text-[#e8466c] border-[#e8466c]/30"
|
||||||
|
: "bg-transparent text-gray-300 hover:bg-white/5 hover:text-white"
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
Next
|
{item}
|
||||||
<ChevronRight size={16} />
|
</PaginationLink>
|
||||||
</Button>
|
</PaginationItem>
|
||||||
|
)}
|
||||||
|
</React.Fragment>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<PaginationItem>
|
||||||
|
<PaginationNext
|
||||||
|
onClick={() => handlePageChange(Math.min(currentPage + 1, totalPages))}
|
||||||
|
className={cn(
|
||||||
|
"border-white/10 bg-transparent text-gray-300 hover:bg-white/5 hover:text-white",
|
||||||
|
(currentPage === totalPages || totalPages === 0) && "pointer-events-none opacity-50"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</PaginationItem>
|
||||||
|
</PaginationContent>
|
||||||
|
</Pagination>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
+125
-61
@@ -1,5 +1,5 @@
|
|||||||
import { Staff, MediaCategory } from '@/types';
|
import { Staff, MediaCategory } from '@/types';
|
||||||
import { useState, useMemo, useEffect } from 'react';
|
import React, { useState, useMemo, useEffect } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
Search, ArrowUpDown, User, ChevronLeft, ChevronRight, X, Filter,
|
Search, ArrowUpDown, User, ChevronLeft, ChevronRight, X, Filter,
|
||||||
@@ -35,6 +35,15 @@ import {
|
|||||||
} from '@/components/ui/table';
|
} from '@/components/ui/table';
|
||||||
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';
|
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
||||||
|
import {
|
||||||
|
Pagination,
|
||||||
|
PaginationContent,
|
||||||
|
PaginationEllipsis,
|
||||||
|
PaginationItem,
|
||||||
|
PaginationLink,
|
||||||
|
PaginationNext,
|
||||||
|
PaginationPrevious,
|
||||||
|
} from '@/components/ui/pagination';
|
||||||
import Loading from '@/components/ui/loading';
|
import Loading from '@/components/ui/loading';
|
||||||
import { motion, AnimatePresence } from 'motion/react';
|
import { motion, AnimatePresence } from 'motion/react';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
@@ -210,14 +219,50 @@ export default function CastView({ onPersonClick, enabledCategories, itemsPerPag
|
|||||||
return filteredStaff.slice(startIndex, startIndex + itemsPerPage);
|
return filteredStaff.slice(startIndex, startIndex + itemsPerPage);
|
||||||
}, [filteredStaff, currentPage, itemsPerPage]);
|
}, [filteredStaff, currentPage, itemsPerPage]);
|
||||||
|
|
||||||
const handlePrevPage = () => {
|
const handlePageChange = (page: number) => {
|
||||||
setCurrentPage((prev) => Math.max(prev - 1, 1));
|
setCurrentPage(page);
|
||||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
const scrollContainer = document.getElementById('cast-scroll-container');
|
||||||
|
if (scrollContainer) {
|
||||||
|
scrollContainer.scrollTo({ top: 0, behavior: 'smooth' });
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleNextPage = () => {
|
// Generate pagination items with ellipsis
|
||||||
setCurrentPage((prev) => Math.min(prev + 1, totalPages));
|
const getPaginationItems = () => {
|
||||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
const items: (number | string)[] = [];
|
||||||
|
const maxVisible = 5;
|
||||||
|
|
||||||
|
if (totalPages <= maxVisible) {
|
||||||
|
for (let i = 1; i <= totalPages; i++) {
|
||||||
|
items.push(i);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Always show first page
|
||||||
|
items.push(1);
|
||||||
|
|
||||||
|
if (currentPage > 3) {
|
||||||
|
items.push('ellipsis-start');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show pages around current
|
||||||
|
const start = Math.max(2, currentPage - 1);
|
||||||
|
const end = Math.min(totalPages - 1, currentPage + 1);
|
||||||
|
|
||||||
|
for (let i = start; i <= end; i++) {
|
||||||
|
items.push(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentPage < totalPages - 2) {
|
||||||
|
items.push('ellipsis-end');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always show last page
|
||||||
|
if (totalPages > 1) {
|
||||||
|
items.push(totalPages);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return items;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Persist view mode
|
// Persist view mode
|
||||||
@@ -246,9 +291,11 @@ export default function CastView({ onPersonClick, enabledCategories, itemsPerPag
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<div className="pt-16 pb-12 px-4 sm:px-6 max-w-[1920px] mx-auto">
|
<div className="flex flex-col h-[calc(100vh-4rem-4rem)] w-full">
|
||||||
|
{/* Sticky Header - Filters */}
|
||||||
|
<div className="px-6 pt-4 pb-4 bg-background border-b border-white/10 shrink-0 z-10">
|
||||||
{/* Compact Toolbar - Like MediaFilters */}
|
{/* Compact Toolbar - Like MediaFilters */}
|
||||||
<div className="flex flex-col gap-4 mb-6">
|
<div className="flex flex-col gap-4">
|
||||||
{/* Top Row: Search, View Toggle, Count */}
|
{/* Top Row: Search, View Toggle, Count */}
|
||||||
<div className="flex items-center gap-2 flex-wrap">
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
{/* Search */}
|
{/* Search */}
|
||||||
@@ -468,7 +515,10 @@ export default function CastView({ onPersonClick, enabledCategories, itemsPerPag
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Scrollable Content Area */}
|
||||||
|
<div id="cast-scroll-container" className="flex-1 overflow-y-auto px-6 pt-4 pb-20">
|
||||||
{/* Content Area */}
|
{/* Content Area */}
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<Loading message="Loading cast..." />
|
<Loading message="Loading cast..." />
|
||||||
@@ -566,8 +616,7 @@ export default function CastView({ onPersonClick, enabledCategories, itemsPerPag
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
/* Table View */
|
/* Table View */
|
||||||
<div className="w-full">
|
<Table className="w-full table-fixed">
|
||||||
<Table className="border rounded-lg border-border/60 w-full table-fixed">
|
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow className="hover:bg-transparent border-border/60 bg-muted/30">
|
<TableRow className="hover:bg-transparent border-border/60 bg-muted/30">
|
||||||
<TableHead className="w-14 rounded-tl-lg"></TableHead>
|
<TableHead className="w-14 rounded-tl-lg"></TableHead>
|
||||||
@@ -689,64 +738,79 @@ export default function CastView({ onPersonClick, enabledCategories, itemsPerPag
|
|||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Pagination - Modern */}
|
{/* End of scrollable content area */}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Sticky Pagination Controls */}
|
||||||
{filteredStaff.length > 0 && (
|
{filteredStaff.length > 0 && (
|
||||||
<div className="mt-10 flex flex-col sm:flex-row items-center justify-between gap-4 pt-6 border-t border-border/50">
|
<div className="px-6 py-4 border-t border-white/10 bg-background shrink-0 z-10">
|
||||||
<div className="flex items-center gap-3 text-sm text-muted-foreground">
|
<div className="flex flex-col sm:flex-row items-center justify-between gap-4">
|
||||||
<span>Showing</span>
|
<div className="flex items-center gap-4">
|
||||||
<span className="font-semibold text-foreground">
|
<span className="text-sm text-gray-500 font-medium">Items per page:</span>
|
||||||
{Math.min((currentPage - 1) * itemsPerPage + 1, filteredStaff.length)}-{Math.min(currentPage * itemsPerPage, filteredStaff.length)}
|
<select
|
||||||
</span>
|
value={itemsPerPage}
|
||||||
<span>of</span>
|
onChange={(e) => {
|
||||||
<span className="font-semibold text-foreground">{filteredStaff.length}</span>
|
setItemsPerPage(Number(e.target.value));
|
||||||
<span>items</span>
|
setCurrentPage(1);
|
||||||
</div>
|
}}
|
||||||
|
className="bg-[#1a1d26] border border-white/10 rounded-md px-2 py-1 text-sm font-medium text-gray-300 focus:ring-2 focus:ring-[#e8466c] outline-none"
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Select
|
|
||||||
value={itemsPerPage.toString()}
|
|
||||||
onValueChange={(value) => setItemsPerPage(Number(value))}
|
|
||||||
>
|
>
|
||||||
<SelectTrigger className="w-20 h-9 rounded-lg text-xs">
|
{[12, 20, 36, 48, 60, 100].map(size => (
|
||||||
<SelectValue />
|
<option key={size} value={size}>{size}</option>
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{[12, 20, 36, 48, 60].map(size => (
|
|
||||||
<SelectItem key={size} value={size.toString()}>{size}</SelectItem>
|
|
||||||
))}
|
))}
|
||||||
</SelectContent>
|
</select>
|
||||||
</Select>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-1 ml-2">
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="icon"
|
|
||||||
onClick={handlePrevPage}
|
|
||||||
disabled={currentPage === 1}
|
|
||||||
className="h-9 w-9 rounded-lg border-border/60"
|
|
||||||
>
|
|
||||||
<ChevronLeft size={16} />
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-1 px-2">
|
|
||||||
<span className="text-sm font-semibold text-[#6d28d9]">{currentPage}</span>
|
|
||||||
<span className="text-sm text-muted-foreground">/</span>
|
|
||||||
<span className="text-sm text-muted-foreground">{totalPages}</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button
|
<Pagination>
|
||||||
variant="outline"
|
<PaginationContent>
|
||||||
size="icon"
|
<PaginationItem>
|
||||||
onClick={handleNextPage}
|
<PaginationPrevious
|
||||||
disabled={currentPage === totalPages || totalPages === 0}
|
onClick={() => handlePageChange(Math.max(currentPage - 1, 1))}
|
||||||
className="h-9 w-9 rounded-lg border-border/60"
|
className={cn(
|
||||||
|
"border-white/10 bg-transparent text-gray-300 hover:bg-white/5 hover:text-white",
|
||||||
|
currentPage === 1 && "pointer-events-none opacity-50"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</PaginationItem>
|
||||||
|
|
||||||
|
{getPaginationItems().map((item, index) => (
|
||||||
|
<React.Fragment key={index}>
|
||||||
|
{item === 'ellipsis-start' || item === 'ellipsis-end' ? (
|
||||||
|
<PaginationItem>
|
||||||
|
<PaginationEllipsis />
|
||||||
|
</PaginationItem>
|
||||||
|
) : (
|
||||||
|
<PaginationItem>
|
||||||
|
<PaginationLink
|
||||||
|
isActive={currentPage === item}
|
||||||
|
onClick={() => handlePageChange(item as number)}
|
||||||
|
className={cn(
|
||||||
|
"border-white/10",
|
||||||
|
currentPage === item
|
||||||
|
? "bg-[#e8466c]/20 text-[#e8466c] border-[#e8466c]/30"
|
||||||
|
: "bg-transparent text-gray-300 hover:bg-white/5 hover:text-white"
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<ChevronRight size={16} />
|
{item}
|
||||||
</Button>
|
</PaginationLink>
|
||||||
</div>
|
</PaginationItem>
|
||||||
|
)}
|
||||||
|
</React.Fragment>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<PaginationItem>
|
||||||
|
<PaginationNext
|
||||||
|
onClick={() => handlePageChange(Math.min(currentPage + 1, totalPages))}
|
||||||
|
className={cn(
|
||||||
|
"border-white/10 bg-transparent text-gray-300 hover:bg-white/5 hover:text-white",
|
||||||
|
(currentPage === totalPages || totalPages === 0) && "pointer-events-none opacity-50"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</PaginationItem>
|
||||||
|
</PaginationContent>
|
||||||
|
</Pagination>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -122,7 +122,7 @@ export default function DashboardView({ mediaList, onMediaClick, loading = false
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="pt-6 pb-12 px-6 max-w-[1920px] mx-auto">
|
<div className="pt-6 pb-20 px-6 max-w-[1920px] mx-auto">
|
||||||
{/* Welcome Header */}
|
{/* Welcome Header */}
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, y: 20 }}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ export default function DetailView({ media, allMedia, onPersonClick }: DetailVie
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<div className="min-h-screen bg-background pb-16">
|
<div className="min-h-screen bg-background pb-20">
|
||||||
{/* Hero Section - Full height from top behind transparent navbar */}
|
{/* Hero Section - Full height from top behind transparent navbar */}
|
||||||
<div className="relative h-[40vh] md:h-[45vh] overflow-hidden bg-zinc-900">
|
<div className="relative h-[40vh] md:h-[45vh] overflow-hidden bg-zinc-900">
|
||||||
<img
|
<img
|
||||||
|
|||||||
@@ -117,8 +117,7 @@ export default function MediaTable({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full bg-[#0d0f14] rounded-lg border border-white/5 overflow-hidden">
|
<Table className="w-full">
|
||||||
<Table>
|
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow className="border-b border-white/[0.03] hover:bg-transparent">
|
<TableRow className="border-b border-white/[0.03] hover:bg-transparent">
|
||||||
<TableHead
|
<TableHead
|
||||||
@@ -261,6 +260,5 @@ export default function MediaTable({
|
|||||||
})}
|
})}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,130 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
import { ChevronLeftIcon, ChevronRightIcon, MoreHorizontalIcon } from "lucide-react"
|
||||||
|
|
||||||
|
function Pagination({ className, ...props }: React.ComponentProps<"nav">) {
|
||||||
|
return (
|
||||||
|
<nav
|
||||||
|
role="navigation"
|
||||||
|
aria-label="pagination"
|
||||||
|
data-slot="pagination"
|
||||||
|
className={cn("mx-auto flex w-full justify-center", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function PaginationContent({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"ul">) {
|
||||||
|
return (
|
||||||
|
<ul
|
||||||
|
data-slot="pagination-content"
|
||||||
|
className={cn("flex items-center gap-0.5", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function PaginationItem({ ...props }: React.ComponentProps<"li">) {
|
||||||
|
return <li data-slot="pagination-item" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
type PaginationLinkProps = {
|
||||||
|
isActive?: boolean
|
||||||
|
} & Pick<React.ComponentProps<typeof Button>, "size"> &
|
||||||
|
React.ComponentProps<"a">
|
||||||
|
|
||||||
|
function PaginationLink({
|
||||||
|
className,
|
||||||
|
isActive,
|
||||||
|
size = "icon",
|
||||||
|
...props
|
||||||
|
}: PaginationLinkProps) {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
variant={isActive ? "outline" : "ghost"}
|
||||||
|
size={size}
|
||||||
|
className={cn(className)}
|
||||||
|
nativeButton={false}
|
||||||
|
render={
|
||||||
|
<a
|
||||||
|
aria-current={isActive ? "page" : undefined}
|
||||||
|
data-slot="pagination-link"
|
||||||
|
data-active={isActive}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function PaginationPrevious({
|
||||||
|
className,
|
||||||
|
text = "Previous",
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof PaginationLink> & { text?: string }) {
|
||||||
|
return (
|
||||||
|
<PaginationLink
|
||||||
|
aria-label="Go to previous page"
|
||||||
|
size="default"
|
||||||
|
className={cn("pl-1.5!", className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ChevronLeftIcon data-icon="inline-start" />
|
||||||
|
<span className="hidden sm:block">{text}</span>
|
||||||
|
</PaginationLink>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function PaginationNext({
|
||||||
|
className,
|
||||||
|
text = "Next",
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof PaginationLink> & { text?: string }) {
|
||||||
|
return (
|
||||||
|
<PaginationLink
|
||||||
|
aria-label="Go to next page"
|
||||||
|
size="default"
|
||||||
|
className={cn("pr-1.5!", className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<span className="hidden sm:block">{text}</span>
|
||||||
|
<ChevronRightIcon data-icon="inline-end" />
|
||||||
|
</PaginationLink>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function PaginationEllipsis({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"span">) {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
aria-hidden
|
||||||
|
data-slot="pagination-ellipsis"
|
||||||
|
className={cn(
|
||||||
|
"flex size-8 items-center justify-center [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<MoreHorizontalIcon
|
||||||
|
/>
|
||||||
|
<span className="sr-only">More pages</span>
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Pagination,
|
||||||
|
PaginationContent,
|
||||||
|
PaginationEllipsis,
|
||||||
|
PaginationItem,
|
||||||
|
PaginationLink,
|
||||||
|
PaginationNext,
|
||||||
|
PaginationPrevious,
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user