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