11import { Badge } from "@/components/ui/badge" ;
2- import { Button } from "@/components/ui/button" ;
32import { Card , CardContent , CardDescription , CardHeader , CardTitle } from "@/components/ui/card" ;
43import { Table , TableBody , TableCell , TableHead , TableHeader , TableRow } from "@/components/ui/table" ;
54import { cn } from "@/lib/utils" ;
65import type { Job , JobsMeta } from "@/types/jobs" ;
76import { useState } from "react" ;
8- import { FiCheckSquare , FiSquare } from "react-icons/fi" ;
7+ import { FiCheckSquare , FiChevronLeft , FiChevronRight , FiSquare } from "react-icons/fi" ;
98
109interface JobsTableCardProps {
1110 meta : JobsMeta ;
@@ -29,7 +28,27 @@ type StatusToggleButtonProps = {
2928 onClick : ( ) => void ;
3029} ;
3130
32- const PAGE_SIZE_OPTIONS = [ 15 , 25 , 50 , 100 ] ;
31+ type PaginationItem = number | string ;
32+
33+ function getResultsLabel ( total : number ) {
34+ return `Resultados: ${ total } ${ total === 1 ? "vaga encontrada" : "vagas encontradas" } ` ;
35+ }
36+
37+ function getPaginationItems ( currentPage : number , totalPages : number ) : PaginationItem [ ] {
38+ if ( totalPages <= 5 ) {
39+ return Array . from ( { length : totalPages } , ( _ , index ) => index + 1 ) ;
40+ }
41+
42+ if ( currentPage <= 3 ) {
43+ return [ 1 , 2 , 3 , 4 , 5 ] ;
44+ }
45+
46+ if ( currentPage >= totalPages - 2 ) {
47+ return Array . from ( { length : 5 } , ( _ , index ) => totalPages - 4 + index ) ;
48+ }
49+
50+ return [ 1 , "ellipsis-start" , currentPage - 1 , currentPage , currentPage + 1 , "ellipsis-end" , totalPages ] ;
51+ }
3352
3453function getJobId ( job : Job , index : number ) {
3554 return job . link ?. trim ( ) || [ job . titulo , job . empresa , job . local , String ( index ) ] . filter ( Boolean ) . join ( "|" ) ;
@@ -79,14 +98,14 @@ export function JobsTableCard({
7998} : JobsTableCardProps ) {
8099 const [ spamMarks , setSpamMarks ] = useState < Record < string , boolean > > ( { } ) ;
81100 const [ readMarks , setReadMarks ] = useState < Record < string , boolean > > ( { } ) ;
101+ const resultsLabel = getResultsLabel ( filteredJobs . length ) ;
102+ const paginationItems = getPaginationItems ( currentPage , totalPages ) ;
82103
83104 return (
84105 < Card className = "mb-4 border-border/70 bg-card/90 backdrop-blur dark:bg-card/95" >
85106 < CardHeader >
86107 < CardTitle className = "text-lg" > Vagas Encontradas</ CardTitle >
87- < CardDescription >
88- Atualizado em { formatDate ( meta . modifiedAt ) } . Mostrando { filteredJobs . length } de { jobs . length } vagas.
89- </ CardDescription >
108+ < CardDescription > Lista paginada com os resultados mais recentes das buscas.</ CardDescription >
90109 </ CardHeader >
91110 < CardContent >
92111 { error ? (
@@ -95,53 +114,14 @@ export function JobsTableCard({
95114 </ div >
96115 ) : null }
97116
98- < div className = "mb-4 flex flex-wrap items-center justify-between gap-3" >
99- < p className = "text-sm text-muted-foreground" >
100- Pagina { currentPage } de { totalPages }
101- </ p >
102- < div className = "flex items-center gap-2" >
103- < label className = "text-sm text-muted-foreground" htmlFor = "page-size" >
104- Itens por pagina
105- </ label >
106- < select
107- id = "page-size"
108- className = "h-9 rounded-md border border-input bg-background px-2 text-sm"
109- value = { pageSize }
110- onChange = { ( event ) => onPageSizeChange ( Number ( event . target . value ) ) }
111- >
112- { PAGE_SIZE_OPTIONS . map ( ( option ) => (
113- < option key = { option } value = { option } >
114- { option }
115- </ option >
116- ) ) }
117- </ select >
118- < Button
119- variant = "outline"
120- size = "sm"
121- onClick = { ( ) => onPageChange ( Math . max ( 1 , currentPage - 1 ) ) }
122- disabled = { currentPage <= 1 }
123- >
124- Anterior
125- </ Button >
126- < Button
127- variant = "outline"
128- size = "sm"
129- onClick = { ( ) => onPageChange ( Math . min ( totalPages , currentPage + 1 ) ) }
130- disabled = { currentPage >= totalPages }
131- >
132- Proxima
133- </ Button >
134- </ div >
135- </ div >
136-
137117 < Table >
138118 < TableHeader >
139119 < TableRow >
140- < TableHead > Titulo </ TableHead >
120+ < TableHead > Título </ TableHead >
141121 < TableHead > Empresa</ TableHead >
142122 < TableHead > Local</ TableHead >
143123 < TableHead > Link</ TableHead >
144- < TableHead > Palavras chaves </ TableHead >
124+ < TableHead > Palavras-chave </ TableHead >
145125 < TableHead className = "text-center text-[11px] uppercase tracking-[0.16em]" > Spam</ TableHead >
146126 < TableHead className = "text-center text-[11px] uppercase tracking-[0.16em]" > Lido</ TableHead >
147127 < TableHead > Fonte</ TableHead >
@@ -225,6 +205,84 @@ export function JobsTableCard({
225205 ) : null }
226206 </ TableBody >
227207 </ Table >
208+
209+ < div className = "mt-4 border-t border-border/60 pt-4" >
210+ < p className = "text-sm text-muted-foreground" >
211+ < span className = "font-medium text-foreground" > { resultsLabel } </ span >
212+ < span > (Atualizado em { formatDate ( meta . modifiedAt ) } )</ span >
213+ </ p >
214+
215+ < div className = "mt-3 flex flex-col gap-3 md:flex-row md:items-center md:justify-between" >
216+ < label
217+ htmlFor = "page-size"
218+ className = "flex w-fit items-center gap-2 rounded-lg border border-border bg-background px-3 py-2 text-sm shadow-sm"
219+ >
220+ < span className = "text-muted-foreground" > Itens por página</ span >
221+ < input
222+ id = "page-size"
223+ aria-label = "Itens por página"
224+ type = "number"
225+ min = { 1 }
226+ max = { 10 }
227+ step = { 1 }
228+ className = "w-14 rounded-md bg-transparent text-center font-semibold text-foreground outline-none"
229+ value = { pageSize }
230+ onChange = { ( event ) => onPageSizeChange ( Number ( event . target . value ) ) }
231+ />
232+ </ label >
233+
234+ < nav className = "flex items-center gap-1 self-end md:self-auto" aria-label = "Paginação" >
235+ < button
236+ type = "button"
237+ aria-label = "Pagina anterior"
238+ onClick = { ( ) => onPageChange ( Math . max ( 1 , currentPage - 1 ) ) }
239+ disabled = { currentPage === 1 }
240+ className = "flex h-9 min-w-9 items-center justify-center rounded-full text-muted-foreground transition-colors hover:bg-muted hover:text-foreground disabled:cursor-not-allowed disabled:opacity-40"
241+ >
242+ < FiChevronLeft className = "h-4 w-4" />
243+ </ button >
244+
245+ { paginationItems . map ( ( item , index ) => {
246+ if ( typeof item !== "number" ) {
247+ return (
248+ < span key = { `${ item } -${ index } ` } className = "px-2 text-sm text-muted-foreground" aria-hidden = "true" >
249+ …
250+ </ span >
251+ ) ;
252+ }
253+
254+ const isCurrent = item === currentPage ;
255+
256+ return (
257+ < button
258+ key = { item }
259+ type = "button"
260+ aria-current = { isCurrent ? "page" : undefined }
261+ onClick = { ( ) => onPageChange ( item ) }
262+ className = { cn (
263+ "flex h-9 min-w-9 items-center justify-center rounded-full px-3 text-sm font-medium transition-colors" ,
264+ isCurrent
265+ ? "bg-[#0c6b35] text-white shadow-sm"
266+ : "text-muted-foreground hover:bg-muted hover:text-foreground" ,
267+ ) }
268+ >
269+ { item }
270+ </ button >
271+ ) ;
272+ } ) }
273+
274+ < button
275+ type = "button"
276+ aria-label = "Pagina seguinte"
277+ onClick = { ( ) => onPageChange ( Math . min ( totalPages , currentPage + 1 ) ) }
278+ disabled = { currentPage === totalPages }
279+ className = "flex h-9 min-w-9 items-center justify-center rounded-full text-muted-foreground transition-colors hover:bg-muted hover:text-foreground disabled:cursor-not-allowed disabled:opacity-40"
280+ >
281+ < FiChevronRight className = "h-4 w-4" />
282+ </ button >
283+ </ nav >
284+ </ div >
285+ </ div >
228286 </ CardContent >
229287 </ Card >
230288 ) ;
0 commit comments