diff --git a/src/components/AppPagination.tsx b/src/components/AppPagination.tsx
new file mode 100644
index 0000000..84a72a1
--- /dev/null
+++ b/src/components/AppPagination.tsx
@@ -0,0 +1,147 @@
+import {
+ Pagination,
+ PaginationContent,
+ PaginationEllipsis,
+ PaginationItem,
+ PaginationLink,
+ PaginationNext,
+ PaginationPrevious,
+} from '@/components/ui/pagination'
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from '@/components/ui/select'
+
+interface AppPaginationProps {
+ currentPage: number
+ totalPages: number
+ totalCount: number
+ pageSize: number
+ pageSizeOptions?: number[]
+ onPageChange: (page: number) => void
+ onPageSizeChange: (size: number) => void
+ itemName?: string // e.g., "items", "extractions", "playlists"
+}
+
+export function AppPagination({
+ currentPage,
+ totalPages,
+ totalCount,
+ pageSize,
+ pageSizeOptions = [10, 20, 50, 100],
+ onPageChange,
+ onPageSizeChange,
+ itemName = 'items',
+}: AppPaginationProps) {
+ // Don't render if there are no items
+ if (totalCount === 0) return null
+
+ const getVisiblePages = () => {
+ const delta = 2
+ const range = []
+ const rangeWithDots = []
+
+ for (
+ let i = Math.max(2, currentPage - delta);
+ i <= Math.min(totalPages - 1, currentPage + delta);
+ i++
+ ) {
+ range.push(i)
+ }
+
+ if (currentPage - delta > 2) {
+ rangeWithDots.push(1, '...')
+ } else {
+ rangeWithDots.push(1)
+ }
+
+ rangeWithDots.push(...range)
+
+ if (currentPage + delta < totalPages - 1) {
+ rangeWithDots.push('...', totalPages)
+ } else if (totalPages > 1) {
+ rangeWithDots.push(totalPages)
+ }
+
+ return rangeWithDots
+ }
+
+ const startItem = Math.min((currentPage - 1) * pageSize + 1, totalCount)
+ const endItem = Math.min(currentPage * pageSize, totalCount)
+
+ return (
+
+
+ Showing {startItem} to {endItem} of {totalCount} {itemName}
+
+
+
+
+
+ {
+ e.preventDefault()
+ if (currentPage > 1) onPageChange(currentPage - 1)
+ }}
+ className={currentPage <= 1 ? 'pointer-events-none opacity-50' : ''}
+ />
+
+
+ {getVisiblePages().map((page, index) => (
+
+ {page === '...' ? (
+
+ ) : (
+ {
+ e.preventDefault()
+ onPageChange(page as number)
+ }}
+ isActive={currentPage === page}
+ >
+ {page}
+
+ )}
+
+ ))}
+
+
+ {
+ e.preventDefault()
+ if (currentPage < totalPages) onPageChange(currentPage + 1)
+ }}
+ className={currentPage >= totalPages ? 'pointer-events-none opacity-50' : ''}
+ />
+
+
+
+
+
+ Show
+
+ rows
+
+
+ )
+}
\ No newline at end of file
diff --git a/src/lib/api/services/extractions.ts b/src/lib/api/services/extractions.ts
index 78535c5..496ba94 100644
--- a/src/lib/api/services/extractions.ts
+++ b/src/lib/api/services/extractions.ts
@@ -22,6 +22,10 @@ export interface CreateExtractionResponse {
export interface GetExtractionsResponse {
extractions: ExtractionInfo[]
+ total: number
+ page: number
+ limit: number
+ total_pages: number
}
export type ExtractionSortField = 'title' | 'status' | 'service' | 'created_at' | 'updated_at'
@@ -33,6 +37,8 @@ export interface GetExtractionsParams {
sort_by?: ExtractionSortField
sort_order?: ExtractionSortOrder
status_filter?: ExtractionStatus
+ page?: number
+ limit?: number
}
export class ExtractionsService {
@@ -59,7 +65,7 @@ export class ExtractionsService {
/**
* Get all extractions
*/
- async getAllExtractions(params?: GetExtractionsParams): Promise {
+ async getAllExtractions(params?: GetExtractionsParams): Promise {
const searchParams = new URLSearchParams()
if (params?.search) {
@@ -74,18 +80,24 @@ export class ExtractionsService {
if (params?.status_filter) {
searchParams.append('status_filter', params.status_filter)
}
+ if (params?.page) {
+ searchParams.append('page', params.page.toString())
+ }
+ if (params?.limit) {
+ searchParams.append('limit', params.limit.toString())
+ }
const queryString = searchParams.toString()
const url = queryString ? `/api/v1/extractions/?${queryString}` : '/api/v1/extractions/'
const response = await apiClient.get(url)
- return response.extractions
+ return response
}
/**
* Get user's extractions
*/
- async getUserExtractions(params?: GetExtractionsParams): Promise {
+ async getUserExtractions(params?: GetExtractionsParams): Promise {
const searchParams = new URLSearchParams()
if (params?.search) {
@@ -100,12 +112,18 @@ export class ExtractionsService {
if (params?.status_filter) {
searchParams.append('status_filter', params.status_filter)
}
+ if (params?.page) {
+ searchParams.append('page', params.page.toString())
+ }
+ if (params?.limit) {
+ searchParams.append('limit', params.limit.toString())
+ }
const queryString = searchParams.toString()
const url = queryString ? `/api/v1/extractions/user?${queryString}` : '/api/v1/extractions/user'
const response = await apiClient.get(url)
- return response.extractions
+ return response
}
}
diff --git a/src/lib/api/services/playlists.ts b/src/lib/api/services/playlists.ts
index c76631d..396e642 100644
--- a/src/lib/api/services/playlists.ts
+++ b/src/lib/api/services/playlists.ts
@@ -45,16 +45,24 @@ export interface GetPlaylistsParams {
search?: string
sort_by?: PlaylistSortField
sort_order?: SortOrder
+ page?: number
limit?: number
- offset?: number
favorites_only?: boolean
}
+export interface GetPlaylistsResponse {
+ playlists: Playlist[]
+ total: number
+ page: number
+ limit: number
+ total_pages: number
+}
+
export class PlaylistsService {
/**
* Get all playlists with optional filtering, searching, and sorting
*/
- async getPlaylists(params?: GetPlaylistsParams): Promise {
+ async getPlaylists(params?: GetPlaylistsParams): Promise {
const searchParams = new URLSearchParams()
// Handle parameters
@@ -67,12 +75,12 @@ export class PlaylistsService {
if (params?.sort_order) {
searchParams.append('sort_order', params.sort_order)
}
+ if (params?.page) {
+ searchParams.append('page', params.page.toString())
+ }
if (params?.limit) {
searchParams.append('limit', params.limit.toString())
}
- if (params?.offset) {
- searchParams.append('offset', params.offset.toString())
- }
if (params?.favorites_only) {
searchParams.append('favorites_only', 'true')
}
@@ -80,7 +88,7 @@ export class PlaylistsService {
const url = searchParams.toString()
? `/api/v1/playlists/?${searchParams.toString()}`
: '/api/v1/playlists/'
- return apiClient.get(url)
+ return apiClient.get(url)
}
/**
diff --git a/src/pages/ExtractionsPage.tsx b/src/pages/ExtractionsPage.tsx
index 22f9624..79c576b 100644
--- a/src/pages/ExtractionsPage.tsx
+++ b/src/pages/ExtractionsPage.tsx
@@ -1,4 +1,5 @@
import { AppLayout } from '@/components/AppLayout'
+import { AppPagination } from '@/components/AppPagination'
import { CreateExtractionDialog } from '@/components/extractions/CreateExtractionDialog'
import { ExtractionsHeader } from '@/components/extractions/ExtractionsHeader'
import {
@@ -28,6 +29,12 @@ export function ExtractionsPage() {
const [sortOrder, setSortOrder] = useState('desc')
const [statusFilter, setStatusFilter] = useState('all')
+ // Pagination state
+ const [currentPage, setCurrentPage] = useState(1)
+ const [totalPages, setTotalPages] = useState(1)
+ const [totalCount, setTotalCount] = useState(0)
+ const [pageSize, setPageSize] = useState(10)
+
// Create extraction dialog state
const [showCreateDialog, setShowCreateDialog] = useState(false)
const [createLoading, setCreateLoading] = useState(false)
@@ -48,13 +55,17 @@ export function ExtractionsPage() {
try {
setLoading(true)
setError(null)
- const data = await extractionsService.getAllExtractions({
+ const response = await extractionsService.getAllExtractions({
search: debouncedSearchQuery.trim() || undefined,
sort_by: sortBy,
sort_order: sortOrder,
status_filter: statusFilter !== 'all' ? statusFilter : undefined,
+ page: currentPage,
+ limit: pageSize,
})
- setExtractions(data)
+ setExtractions(response.extractions)
+ setTotalPages(response.total_pages)
+ setTotalCount(response.total)
} catch (err) {
const errorMessage =
err instanceof Error ? err.message : 'Failed to fetch extractions'
@@ -67,7 +78,24 @@ export function ExtractionsPage() {
useEffect(() => {
fetchExtractions()
- }, [debouncedSearchQuery, sortBy, sortOrder, statusFilter])
+ }, [debouncedSearchQuery, sortBy, sortOrder, statusFilter, currentPage, pageSize])
+
+ // Reset to page 1 when filters change
+ useEffect(() => {
+ if (currentPage !== 1) {
+ setCurrentPage(1)
+ }
+ }, [debouncedSearchQuery, sortBy, sortOrder, statusFilter, pageSize])
+
+ const handlePageChange = (page: number) => {
+ setCurrentPage(page)
+ }
+
+ const handlePageSizeChange = (size: number) => {
+ setPageSize(size)
+ setCurrentPage(1) // Reset to first page when changing page size
+ }
+
const handleCreateExtraction = async () => {
if (!url.trim()) {
@@ -113,7 +141,20 @@ export function ExtractionsPage() {
return
}
- return
+ return (
+
+ )
}
return (
@@ -136,7 +177,7 @@ export function ExtractionsPage() {
onCreateClick={() => setShowCreateDialog(true)}
loading={loading}
error={error}
- extractionCount={extractions.length}
+ extractionCount={totalCount}
/>
('asc')
const [showFavoritesOnly, setShowFavoritesOnly] = useState(false)
+ // Pagination state
+ const [currentPage, setCurrentPage] = useState(1)
+ const [totalPages, setTotalPages] = useState(1)
+ const [totalCount, setTotalCount] = useState(0)
+ const [pageSize, setPageSize] = useState(10)
+
// Create playlist dialog state
const [showCreateDialog, setShowCreateDialog] = useState(false)
const [createLoading, setCreateLoading] = useState(false)
@@ -54,13 +61,17 @@ export function PlaylistsPage() {
try {
setLoading(true)
setError(null)
- const playlistData = await playlistsService.getPlaylists({
+ const response = await playlistsService.getPlaylists({
search: debouncedSearchQuery.trim() || undefined,
sort_by: sortBy,
sort_order: sortOrder,
favorites_only: showFavoritesOnly,
+ page: currentPage,
+ limit: pageSize,
})
- setPlaylists(playlistData)
+ setPlaylists(response.playlists)
+ setTotalPages(response.total_pages)
+ setTotalCount(response.total)
} catch (err) {
const errorMessage =
err instanceof Error ? err.message : 'Failed to fetch playlists'
@@ -73,7 +84,23 @@ export function PlaylistsPage() {
useEffect(() => {
fetchPlaylists()
- }, [debouncedSearchQuery, sortBy, sortOrder, showFavoritesOnly])
+ }, [debouncedSearchQuery, sortBy, sortOrder, showFavoritesOnly, currentPage, pageSize])
+
+ // Reset to page 1 when filters change
+ useEffect(() => {
+ if (currentPage !== 1) {
+ setCurrentPage(1)
+ }
+ }, [debouncedSearchQuery, sortBy, sortOrder, showFavoritesOnly, pageSize])
+
+ const handlePageChange = (page: number) => {
+ setCurrentPage(page)
+ }
+
+ const handlePageSizeChange = (size: number) => {
+ setPageSize(size)
+ setCurrentPage(1) // Reset to first page when changing page size
+ }
const handleCreatePlaylist = async () => {
if (!newPlaylist.name.trim()) {
@@ -176,12 +203,23 @@ export function PlaylistsPage() {
}
return (
-
+
)
}
@@ -203,7 +241,7 @@ export function PlaylistsPage() {
onCreateClick={() => setShowCreateDialog(true)}
loading={loading}
error={error}
- playlistCount={playlists.length}
+ playlistCount={totalCount}
showFavoritesOnly={showFavoritesOnly}
onFavoritesToggle={setShowFavoritesOnly}
/>