11"use client"
22
3- import React , { useEffect , useMemo , useRef , useState } from 'react'
4- import { useSearchParams } from 'next/navigation'
3+ import React from 'react'
54import JobsTable from '@/components/jobs/JobsTable'
65import type { ApiResponse , JobsListData , JobWithRelations } from '@/lib/jobs/types'
7-
8- type JobsFetchResult = {
9- items : JobWithRelations [ ]
10- totalCount : number
11- }
6+ import type { PaginatedResponse } from '@/lib/pagination'
7+ import { usePagination } from '@/lib/pagination'
128
139const JOBS_PER_PAGE = 10
1410const POLL_INTERVAL_MS = 6000
15- const MIN_REFRESH_INDICATOR_MS = 500
1611
17- async function fetchJobsPage (
18- page : number ,
19- limit : number ,
20- signal ?: AbortSignal
21- ) : Promise < JobsFetchResult > {
22- const res = await fetch ( `https://api.codebuilder.org/jobs?page=${ page } &limit=${ limit } ` , { signal } )
12+ /**
13+ * Fetch function for jobs API using offset-based pagination
14+ */
15+ async function fetchJobs ( url : string , signal ?: AbortSignal ) : Promise < PaginatedResponse < JobWithRelations > > {
16+ const res = await fetch ( url , { signal } )
2317 const json : ApiResponse < JobsListData > | any = await res . json ( )
2418
25- // Primary shape (current backend): { success: true, data: { items: [...], totalCount } }
19+ // Primary shape (current backend): { success: true, data: { items: [...], totalCount, pageInfo } }
2620 if ( json ?. success === true && Array . isArray ( json ?. data ?. items ) ) {
2721 return {
2822 items : json . data . items ,
2923 totalCount : typeof json . data . totalCount === 'number' ? json . data . totalCount : 0 ,
24+ pageInfo : json . data . pageInfo ,
3025 }
3126 }
3227
@@ -42,129 +37,21 @@ async function fetchJobsPage(
4237}
4338
4439export default function JobsPageClient ( ) {
45- const searchParams = useSearchParams ( )
46- const [ jobs , setJobs ] = useState < JobWithRelations [ ] > ( [ ] )
47- const [ totalJobs , setTotalJobs ] = useState ( 0 )
48- const [ currentPage , setCurrentPage ] = useState ( 1 )
49- const [ isInitialLoading , setIsInitialLoading ] = useState ( true )
50- const [ isRefreshing , setIsRefreshing ] = useState ( false )
51- const [ refreshSecondsRemaining , setRefreshSecondsRemaining ] = useState < number | null > ( null )
52-
53- const refreshInFlightRef = useRef ( false )
54- const nextRefreshAtRef = useRef < number > ( 0 )
55-
56- const lastIdsSignatureRef = useRef < string > ( '' )
57- const lastTotalCountRef = useRef < number > ( 0 )
58- const didInitialLoadRef = useRef ( false )
59-
60- const postsPerPage = JOBS_PER_PAGE
61-
62- const pageFromParams = useMemo ( ( ) => {
63- return parseInt ( searchParams . get ( 'page' ) || '1' , 10 )
64- } , [ searchParams ] )
65-
66- useEffect ( ( ) => {
67- const controller = new AbortController ( )
68- const page = Number . isFinite ( pageFromParams ) && pageFromParams > 0 ? pageFromParams : 1
69- setCurrentPage ( page )
70-
71- const isFirstEverLoad = ! didInitialLoadRef . current
72- if ( isFirstEverLoad ) setIsInitialLoading ( true )
73-
74- fetchJobsPage ( page , postsPerPage , controller . signal )
75- . then ( ( { items, totalCount } ) => {
76- setJobs ( items )
77- setTotalJobs ( totalCount )
78- lastTotalCountRef . current = totalCount
79- lastIdsSignatureRef . current = items . map ( ( j ) => j . id ) . join ( ',' )
80- } )
81- . catch ( ( ) => {
82- setJobs ( [ ] )
83- setTotalJobs ( 0 )
84- lastTotalCountRef . current = 0
85- lastIdsSignatureRef . current = ''
86- } )
87- . finally ( ( ) => {
88- didInitialLoadRef . current = true
89- setIsInitialLoading ( false )
90- } )
91-
92- return ( ) => controller . abort ( )
93- } , [ pageFromParams , postsPerPage ] )
94-
95- useEffect ( ( ) => {
96- if ( isInitialLoading ) return
97-
98- let isUnmounted = false
99- const controller = new AbortController ( )
100- let clearRefreshingTimeoutId : number | null = null
101-
102- const tick = async ( ) => {
103- if ( refreshInFlightRef . current ) return
104- refreshInFlightRef . current = true
105- setIsRefreshing ( true )
106- const startedAt = Date . now ( )
107- try {
108- const { items, totalCount } = await fetchJobsPage ( currentPage , postsPerPage , controller . signal )
109- if ( isUnmounted ) return
110-
111- const nextIdsSignature = items . map ( ( j ) => j . id ) . join ( ',' )
112- const totalIncreased = totalCount > lastTotalCountRef . current
113- const rowsChanged = nextIdsSignature !== lastIdsSignatureRef . current
114-
115- if ( totalIncreased || rowsChanged ) {
116- setJobs ( items )
117- setTotalJobs ( totalCount )
118- lastTotalCountRef . current = totalCount
119- lastIdsSignatureRef . current = nextIdsSignature
120- }
121- } catch {
122- // Keep existing jobs on refresh failure; this is a background enhancement.
123- } finally {
124- refreshInFlightRef . current = false
125- if ( ! isUnmounted ) {
126- const elapsed = Date . now ( ) - startedAt
127- const remaining = Math . max ( 0 , MIN_REFRESH_INDICATOR_MS - elapsed )
128-
129- if ( clearRefreshingTimeoutId !== null ) {
130- window . clearTimeout ( clearRefreshingTimeoutId )
131- }
132-
133- clearRefreshingTimeoutId = window . setTimeout ( ( ) => {
134- if ( ! isUnmounted ) setIsRefreshing ( false )
135- } , remaining )
136- }
137- nextRefreshAtRef . current = Date . now ( ) + POLL_INTERVAL_MS
138- }
139- }
140-
141- nextRefreshAtRef . current = Date . now ( ) + POLL_INTERVAL_MS
142-
143- const countdownId = window . setInterval ( ( ) => {
144- const nextAt = nextRefreshAtRef . current
145- if ( ! nextAt ) {
146- setRefreshSecondsRemaining ( null )
147- return
148- }
149- const seconds = Math . max ( 0 , Math . ceil ( ( nextAt - Date . now ( ) ) / 1000 ) )
150- setRefreshSecondsRemaining ( seconds )
151- } , 250 )
152-
153- // Do one immediate background refresh after initial load.
154- void tick ( )
155-
156- const intervalId = window . setInterval ( tick , POLL_INTERVAL_MS )
157- return ( ) => {
158- isUnmounted = true
159- controller . abort ( )
160- window . clearInterval ( intervalId )
161- window . clearInterval ( countdownId )
162-
163- if ( clearRefreshingTimeoutId !== null ) {
164- window . clearTimeout ( clearRefreshingTimeoutId )
165- }
166- }
167- } , [ currentPage , postsPerPage , isInitialLoading ] )
40+ const {
41+ items : jobs ,
42+ paginationState,
43+ isInitialLoading,
44+ isRefreshing,
45+ refreshSecondsRemaining,
46+ } = usePagination < JobWithRelations > ( {
47+ config : {
48+ type : 'offset-based' , // Using offset-based pagination for the API
49+ itemsPerPage : JOBS_PER_PAGE ,
50+ } ,
51+ fetchFn : fetchJobs ,
52+ baseUrl : 'https://api.codebuilder.org/jobs' ,
53+ pollInterval : POLL_INTERVAL_MS ,
54+ } )
16855
16956 return (
17057 < div className = "flex flex-col inset-0 z-50 bg-primary transition-transform" >
@@ -173,9 +60,9 @@ export default function JobsPageClient() {
17360 < h1 className = "text-2xl font-bold mb-4" > Job Listings</ h1 >
17461 < JobsTable
17562 jobs = { jobs }
176- totalJobs = { totalJobs }
177- jobsPerPage = { postsPerPage }
178- currentPage = { currentPage }
63+ totalJobs = { paginationState . totalItems }
64+ jobsPerPage = { paginationState . itemsPerPage }
65+ currentPage = { paginationState . currentPage }
17966 isInitialLoading = { isInitialLoading }
18067 isRefreshing = { isRefreshing }
18168 refreshSecondsRemaining = { refreshSecondsRemaining }
0 commit comments