feat(frontend): implement pagination and adjust seeder jobs count
This commit is contained in:
parent
d369835999
commit
78314f2b45
2 changed files with 66 additions and 24 deletions
|
|
@ -27,6 +27,9 @@ function JobsContent() {
|
|||
const [sortBy, setSortBy] = useState("recent")
|
||||
const [showFilters, setShowFilters] = useState(false)
|
||||
|
||||
const [currentPage, setCurrentPage] = useState(1)
|
||||
const ITEMS_PER_PAGE = 10
|
||||
|
||||
useEffect(() => {
|
||||
let isMounted = true
|
||||
|
||||
|
|
@ -35,7 +38,8 @@ function JobsContent() {
|
|||
setError(null)
|
||||
|
||||
try {
|
||||
const response = await jobsApi.list({ limit: 50, page: 1 })
|
||||
// Fetch many jobs to allow client-side filtering and pagination
|
||||
const response = await jobsApi.list({ limit: 1000, page: 1 })
|
||||
const mappedJobs = response.data.map(transformApiJobToFrontend)
|
||||
|
||||
if (isMounted) {
|
||||
|
|
@ -64,6 +68,11 @@ function JobsContent() {
|
|||
// Debounce search term para otimizar performance
|
||||
const debouncedSearchTerm = useDebounce(searchTerm, 300)
|
||||
|
||||
// Reset page when filters change
|
||||
useEffect(() => {
|
||||
setCurrentPage(1)
|
||||
}, [debouncedSearchTerm, locationFilter, typeFilter, sortBy])
|
||||
|
||||
// Extrair valores únicos para os filtros
|
||||
const uniqueLocations = useMemo(() => {
|
||||
const locations = jobs.map(job => job.location)
|
||||
|
|
@ -108,6 +117,13 @@ function JobsContent() {
|
|||
return filtered
|
||||
}, [debouncedSearchTerm, locationFilter, typeFilter, sortBy, jobs])
|
||||
|
||||
// Pagination Logic
|
||||
const totalPages = Math.ceil(filteredAndSortedJobs.length / ITEMS_PER_PAGE)
|
||||
const paginatedJobs = filteredAndSortedJobs.slice(
|
||||
(currentPage - 1) * ITEMS_PER_PAGE,
|
||||
currentPage * ITEMS_PER_PAGE
|
||||
)
|
||||
|
||||
const hasActiveFilters = searchTerm || locationFilter !== "all" || typeFilter !== "all"
|
||||
|
||||
const clearFilters = () => {
|
||||
|
|
@ -156,7 +172,7 @@ function JobsContent() {
|
|||
className="pl-10 h-12"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
|
|
@ -210,9 +226,9 @@ function JobsContent() {
|
|||
<SelectItem value="all">Todos os tipos</SelectItem>
|
||||
{uniqueTypes.map((type) => (
|
||||
<SelectItem key={type} value={type}>
|
||||
{type === "full-time" ? "Tempo integral" :
|
||||
type === "part-time" ? "Meio período" :
|
||||
type === "contract" ? "Contrato" : type}
|
||||
{type === "full-time" ? "Tempo integral" :
|
||||
type === "part-time" ? "Meio período" :
|
||||
type === "contract" ? "Contrato" : type}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
|
|
@ -252,6 +268,7 @@ function JobsContent() {
|
|||
<div className="flex items-center justify-between text-sm text-muted-foreground">
|
||||
<span>
|
||||
{filteredAndSortedJobs.length} vaga{filteredAndSortedJobs.length !== 1 ? 's' : ''} encontrada{filteredAndSortedJobs.length !== 1 ? 's' : ''}
|
||||
{totalPages > 1 && ` (Página ${currentPage} de ${totalPages})`}
|
||||
</span>
|
||||
{hasActiveFilters && (
|
||||
<div className="flex items-center gap-2">
|
||||
|
|
@ -299,23 +316,48 @@ function JobsContent() {
|
|||
|
||||
{loading ? (
|
||||
<div className="text-center text-muted-foreground">Carregando vagas...</div>
|
||||
) : filteredAndSortedJobs.length > 0 ? (
|
||||
<motion.div layout className="grid gap-6">
|
||||
<AnimatePresence>
|
||||
{filteredAndSortedJobs.map((job, index) => (
|
||||
<motion.div
|
||||
key={job.id}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -20 }}
|
||||
transition={{ delay: index * 0.05 }}
|
||||
layout
|
||||
) : paginatedJobs.length > 0 ? (
|
||||
<div className="space-y-8">
|
||||
<motion.div layout className="grid gap-6">
|
||||
<AnimatePresence mode="popLayout">
|
||||
{paginatedJobs.map((job, index) => (
|
||||
<motion.div
|
||||
key={job.id}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -20 }}
|
||||
transition={{ delay: index * 0.05 }}
|
||||
layout
|
||||
>
|
||||
<JobCard job={job} />
|
||||
</motion.div>
|
||||
))}
|
||||
</AnimatePresence>
|
||||
</motion.div>
|
||||
|
||||
{/* Pagination Controls */}
|
||||
{totalPages > 1 && (
|
||||
<div className="flex justify-center items-center gap-2 mt-8">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setCurrentPage(p => Math.max(1, p - 1))}
|
||||
disabled={currentPage === 1}
|
||||
>
|
||||
<JobCard job={job} />
|
||||
</motion.div>
|
||||
))}
|
||||
</AnimatePresence>
|
||||
</motion.div>
|
||||
Anterior
|
||||
</Button>
|
||||
<div className="text-sm text-muted-foreground px-4">
|
||||
Página {currentPage} de {totalPages}
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setCurrentPage(p => Math.min(totalPages, p + 1))}
|
||||
disabled={currentPage === totalPages}
|
||||
>
|
||||
Próxima
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ function getRandomInt(min, max) {
|
|||
}
|
||||
|
||||
export async function seedJobs() {
|
||||
console.log('💼 Seeding jobs (33 per company = 990 total)...');
|
||||
console.log('💼 Seeding jobs (25 per company)...');
|
||||
|
||||
// Get company IDs
|
||||
const companiesRes = await pool.query('SELECT id, name FROM companies ORDER BY id');
|
||||
|
|
@ -79,8 +79,8 @@ export async function seedJobs() {
|
|||
|
||||
try {
|
||||
for (const company of companies) {
|
||||
// Generate 33 jobs per company
|
||||
for (let i = 0; i < 33; i++) {
|
||||
// Generate 25 jobs per company
|
||||
for (let i = 0; i < 25; i++) {
|
||||
const template = jobTemplates[i % jobTemplates.length];
|
||||
const level = levels[i % levels.length];
|
||||
const workMode = workModes[i % 3]; // Even distribution: onsite, hybrid, remote
|
||||
|
|
|
|||
Loading…
Reference in a new issue