Skip to content

Commit 4885b63

Browse files
feat: refactor App component to utilize custom hooks for data management, filtering, and pagination
1 parent 12e4607 commit 4885b63

4 files changed

Lines changed: 198 additions & 94 deletions

File tree

frontend/src/App.tsx

Lines changed: 42 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1-
import { useCallback, useEffect, useMemo, useState } from "react";
1+
import { useCallback, type SetStateAction } from "react";
22
import { JobsFiltersCard } from "@/components/JobsFiltersCard";
33
import { JobsHeaderCard } from "@/components/JobsHeaderCard";
44
import { JobsTableCard } from "@/components/JobsTableCard";
5-
import { fetchJobFiles, fetchJobsByFile } from "@/services/jobsService";
6-
import type { Job, JobFile, JobsMeta } from "@/types/jobs";
5+
import { useJobsData } from "@/hooks/useJobsData";
6+
import { useJobsFiltering } from "@/hooks/useJobsFiltering";
7+
import { useJobsPagination } from "@/hooks/useJobsPagination";
8+
import type { JobsMeta } from "@/types/jobs";
79

810
function formatDate(timestamp: JobsMeta["modifiedAt"]): string {
911
if (!timestamp) {
@@ -13,100 +15,46 @@ function formatDate(timestamp: JobsMeta["modifiedAt"]): string {
1315
}
1416

1517
function App() {
16-
const [files, setFiles] = useState<JobFile[]>([]);
17-
const [selectedFile, setSelectedFile] = useState("");
18-
const [search, setSearch] = useState("");
19-
const [keywordFilter, setKeywordFilter] = useState("all");
20-
const [currentPage, setCurrentPage] = useState(1);
21-
const [pageSize, setPageSize] = useState(25);
22-
const [jobs, setJobs] = useState<Job[]>([]);
23-
const [meta, setMeta] = useState<JobsMeta>({ file: "", modifiedAt: null, total: 0 });
24-
const [loading, setLoading] = useState(false);
25-
const [error, setError] = useState("");
18+
const { files, selectedFile, setSelectedFile, jobs, meta, loading, error, loadJobs } = useJobsData();
2619

27-
const keywords = useMemo(() => {
28-
const values = Array.from(new Set(jobs.map((job) => String(job.palavra || "").trim()).filter(Boolean)));
29-
return values.sort((a, b) => a.localeCompare(b));
30-
}, [jobs]);
20+
const { search, setSearch, keywordFilter, setKeywordFilter, keywords, filteredJobs } = useJobsFiltering(jobs);
3121

32-
const filteredJobs = useMemo(() => {
33-
const term = search.trim().toLowerCase();
34-
35-
return jobs.filter((job) => {
36-
const byKeyword = keywordFilter === "all" || String(job.palavra || "") === keywordFilter;
37-
if (!byKeyword) {
38-
return false;
39-
}
40-
41-
if (!term) {
42-
return true;
43-
}
44-
45-
const text = [job.titulo, job.empresa, job.local, job.link, job.palavra]
46-
.map((value) => String(value || "").toLowerCase())
47-
.join(" ");
48-
49-
return text.includes(term);
22+
const { currentPage, setCurrentPage, pageSize, setPageSize, resetPagination, totalPages, paginatedJobs } =
23+
useJobsPagination({
24+
filteredJobs,
5025
});
51-
}, [jobs, search, keywordFilter]);
52-
53-
const totalPages = useMemo(() => {
54-
return Math.max(1, Math.ceil(filteredJobs.length / pageSize));
55-
}, [filteredJobs.length, pageSize]);
56-
57-
const paginatedJobs = useMemo(() => {
58-
const start = (currentPage - 1) * pageSize;
59-
return filteredJobs.slice(start, start + pageSize);
60-
}, [filteredJobs, currentPage, pageSize]);
61-
62-
const loadJobs = useCallback(async (fileName: string) => {
63-
setLoading(true);
64-
setError("");
65-
try {
66-
const data = await fetchJobsByFile(fileName);
67-
setJobs(data.jobs);
68-
setMeta({
69-
file: data.file,
70-
modifiedAt: data.modifiedAt,
71-
total: data.total,
72-
});
73-
setSelectedFile(data.file || fileName || "");
74-
} catch (err: unknown) {
75-
setJobs([]);
76-
setMeta({ file: "", modifiedAt: null, total: 0 });
77-
setError(err instanceof Error ? err.message : "Erro inesperado ao carregar vagas.");
78-
} finally {
79-
setLoading(false);
80-
}
81-
}, []);
8226

83-
useEffect(() => {
84-
async function initializeFiles() {
85-
const foundFiles = await fetchJobFiles();
86-
setFiles(foundFiles);
87-
if (foundFiles[0]?.file) {
88-
setSelectedFile(foundFiles[0].file);
89-
}
90-
}
91-
92-
initializeFiles().catch(() => {
93-
setError("Nao foi possivel listar arquivos .xlsx da pasta output.");
94-
});
95-
}, []);
27+
const handleSearchChange = useCallback(
28+
(value: SetStateAction<string>) => {
29+
setSearch((previous) => (typeof value === "function" ? value(previous) : value));
30+
resetPagination();
31+
},
32+
[setSearch, resetPagination],
33+
);
9634

97-
useEffect(() => {
98-
if (selectedFile) {
99-
loadJobs(selectedFile);
100-
}
101-
}, [selectedFile, loadJobs]);
35+
const handleKeywordFilterChange = useCallback(
36+
(value: SetStateAction<string>) => {
37+
setKeywordFilter((previous) => (typeof value === "function" ? value(previous) : value));
38+
resetPagination();
39+
},
40+
[setKeywordFilter, resetPagination],
41+
);
10242

103-
useEffect(() => {
104-
setCurrentPage(1);
105-
}, [search, keywordFilter, selectedFile, pageSize]);
43+
const handleSelectedFileChange = useCallback(
44+
(value: SetStateAction<string>) => {
45+
setSelectedFile((previous) => (typeof value === "function" ? value(previous) : value));
46+
resetPagination();
47+
},
48+
[setSelectedFile, resetPagination],
49+
);
10650

107-
useEffect(() => {
108-
setCurrentPage((previous) => Math.min(previous, totalPages));
109-
}, [totalPages]);
51+
const handlePageSizeChange = useCallback(
52+
(value: number) => {
53+
setPageSize(value);
54+
resetPagination();
55+
},
56+
[setPageSize, resetPagination],
57+
);
11058

11159
return (
11260
<main className="relative min-h-screen overflow-hidden bg-background px-4 py-8 md:px-8">
@@ -117,12 +65,12 @@ function App() {
11765

11866
<JobsFiltersCard
11967
search={search}
120-
setSearch={setSearch}
68+
setSearch={handleSearchChange}
12169
keywordFilter={keywordFilter}
122-
setKeywordFilter={setKeywordFilter}
70+
setKeywordFilter={handleKeywordFilterChange}
12371
keywords={keywords}
12472
selectedFile={selectedFile}
125-
setSelectedFile={setSelectedFile}
73+
setSelectedFile={handleSelectedFileChange}
12674
files={files}
12775
loading={loading}
12876
onRefresh={() => loadJobs(selectedFile)}
@@ -140,7 +88,7 @@ function App() {
14088
totalPages={totalPages}
14189
pageSize={pageSize}
14290
onPageChange={setCurrentPage}
143-
onPageSizeChange={setPageSize}
91+
onPageSizeChange={handlePageSizeChange}
14492
/>
14593
</section>
14694
</main>

frontend/src/hooks/useJobsData.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { useCallback, useEffect, useState } from "react";
2+
import { fetchJobFiles, fetchJobsByFile } from "@/services/jobsService";
3+
import type { Job, JobFile, JobsMeta } from "@/types/jobs";
4+
5+
const EMPTY_META: JobsMeta = { file: "", modifiedAt: null, total: 0 };
6+
7+
export function useJobsData() {
8+
const [files, setFiles] = useState<JobFile[]>([]);
9+
const [selectedFile, setSelectedFile] = useState("");
10+
const [jobs, setJobs] = useState<Job[]>([]);
11+
const [meta, setMeta] = useState<JobsMeta>(EMPTY_META);
12+
const [loading, setLoading] = useState(false);
13+
const [error, setError] = useState("");
14+
15+
const loadJobs = useCallback(async (fileName: string) => {
16+
setLoading(true);
17+
setError("");
18+
19+
try {
20+
const data = await fetchJobsByFile(fileName);
21+
setJobs(data.jobs);
22+
setMeta({
23+
file: data.file,
24+
modifiedAt: data.modifiedAt,
25+
total: data.total,
26+
});
27+
setSelectedFile(data.file || fileName || "");
28+
} catch (err: unknown) {
29+
setJobs([]);
30+
setMeta(EMPTY_META);
31+
setError(err instanceof Error ? err.message : "Erro inesperado ao carregar vagas.");
32+
} finally {
33+
setLoading(false);
34+
}
35+
}, []);
36+
37+
useEffect(() => {
38+
async function initializeFiles() {
39+
const foundFiles = await fetchJobFiles();
40+
setFiles(foundFiles);
41+
if (foundFiles[0]?.file) {
42+
setSelectedFile(foundFiles[0].file);
43+
}
44+
}
45+
46+
initializeFiles().catch(() => {
47+
setError("Nao foi possivel listar arquivos .xlsx da pasta output.");
48+
});
49+
}, []);
50+
51+
useEffect(() => {
52+
if (selectedFile) {
53+
loadJobs(selectedFile);
54+
}
55+
}, [selectedFile, loadJobs]);
56+
57+
return {
58+
files,
59+
selectedFile,
60+
setSelectedFile,
61+
jobs,
62+
meta,
63+
loading,
64+
error,
65+
loadJobs,
66+
};
67+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { useMemo, useState } from "react";
2+
import type { Job } from "@/types/jobs";
3+
4+
export function useJobsFiltering(jobs: Job[]) {
5+
const [search, setSearch] = useState("");
6+
const [keywordFilter, setKeywordFilter] = useState("all");
7+
8+
const keywords = useMemo(() => {
9+
const values = Array.from(new Set(jobs.map((job) => String(job.palavra || "").trim()).filter(Boolean)));
10+
return values.sort((a, b) => a.localeCompare(b));
11+
}, [jobs]);
12+
13+
const filteredJobs = useMemo(() => {
14+
const term = search.trim().toLowerCase();
15+
16+
return jobs.filter((job) => {
17+
const byKeyword = keywordFilter === "all" || String(job.palavra || "") === keywordFilter;
18+
if (!byKeyword) {
19+
return false;
20+
}
21+
22+
if (!term) {
23+
return true;
24+
}
25+
26+
const text = [job.titulo, job.empresa, job.local, job.link, job.palavra]
27+
.map((value) => String(value || "").toLowerCase())
28+
.join(" ");
29+
30+
return text.includes(term);
31+
});
32+
}, [jobs, search, keywordFilter]);
33+
34+
return {
35+
search,
36+
setSearch,
37+
keywordFilter,
38+
setKeywordFilter,
39+
keywords,
40+
filteredJobs,
41+
};
42+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { useMemo, useState } from "react";
2+
import type { Job } from "@/types/jobs";
3+
4+
interface UseJobsPaginationParams {
5+
filteredJobs: Job[];
6+
initialPageSize?: number;
7+
}
8+
9+
export function useJobsPagination({
10+
filteredJobs,
11+
initialPageSize = 25,
12+
}: UseJobsPaginationParams) {
13+
const [page, setPage] = useState(1);
14+
const [pageSize, setPageSize] = useState(initialPageSize);
15+
16+
const totalPages = useMemo(() => {
17+
return Math.max(1, Math.ceil(filteredJobs.length / pageSize));
18+
}, [filteredJobs.length, pageSize]);
19+
20+
const currentPage = Math.min(page, totalPages);
21+
22+
const paginatedJobs = useMemo(() => {
23+
const start = (currentPage - 1) * pageSize;
24+
return filteredJobs.slice(start, start + pageSize);
25+
}, [filteredJobs, currentPage, pageSize]);
26+
27+
function setCurrentPage(value: number | ((previous: number) => number)) {
28+
setPage((previous) => {
29+
const next = typeof value === "function" ? value(previous) : value;
30+
return Math.max(1, next);
31+
});
32+
}
33+
34+
function resetPagination() {
35+
setPage(1);
36+
}
37+
38+
return {
39+
currentPage,
40+
setCurrentPage,
41+
pageSize,
42+
setPageSize,
43+
resetPagination,
44+
totalPages,
45+
paginatedJobs,
46+
};
47+
}

0 commit comments

Comments
 (0)