fix(frontend): harden home jobs loading states

This commit is contained in:
GoHorse Deploy 2026-03-07 11:06:47 -03:00
parent 757429afe6
commit a3febf2087
6 changed files with 63 additions and 15 deletions

View file

@ -229,6 +229,7 @@ export default function AdminJobsPage() {
<DialogContent className="max-w-2xl max-h-[85vh] overflow-y-auto"> <DialogContent className="max-w-2xl max-h-[85vh] overflow-y-auto">
<DialogHeader> <DialogHeader>
<DialogTitle>{t('admin.jobs.details.title')}</DialogTitle> <DialogTitle>{t('admin.jobs.details.title')}</DialogTitle>
<DialogDescription>{t('admin.jobs.details.description')}</DialogDescription>
</DialogHeader> </DialogHeader>
{selectedJob && ( {selectedJob && (
<div className="grid gap-4 py-4"> <div className="grid gap-4 py-4">

View file

@ -19,6 +19,7 @@ export default function Home() {
const { t } = useTranslation() const { t } = useTranslation()
const [jobs, setJobs] = useState<Job[]>([]) const [jobs, setJobs] = useState<Job[]>([])
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const [jobsError, setJobsError] = useState(false)
const [page, setPage] = useState(1) const [page, setPage] = useState(1)
const [hasMore, setHasMore] = useState(true) const [hasMore, setHasMore] = useState(true)
const [loadingMore, setLoadingMore] = useState(false) const [loadingMore, setLoadingMore] = useState(false)
@ -36,10 +37,18 @@ export default function Home() {
const fetchJobs = useCallback(async (pageNum: number, isLoadMore = false) => { const fetchJobs = useCallback(async (pageNum: number, isLoadMore = false) => {
try { try {
if (isLoadMore) setLoadingMore(true) if (isLoadMore) setLoadingMore(true)
else setLoading(true) else {
setLoading(true)
setJobsError(false)
}
const limit = 8 const limit = 8
const res = await jobsApi.list({ page: pageNum, limit }) const res = await Promise.race([
jobsApi.list({ page: pageNum, limit }),
new Promise<never>((_, reject) => {
window.setTimeout(() => reject(new Error("JOBS_REQUEST_TIMEOUT")), 12000)
}),
])
if (res.data) { if (res.data) {
const newJobs = res.data.map(transformApiJobToFrontend) const newJobs = res.data.map(transformApiJobToFrontend)
@ -52,15 +61,22 @@ export default function Home() {
// If we got fewer jobs than the limit, we've reached the end // If we got fewer jobs than the limit, we've reached the end
if (newJobs.length < limit) { if (newJobs.length < limit) {
setHasMore(false) setHasMore(false)
} else if (!isLoadMore) {
setHasMore(true)
} }
} }
} catch (error) { } catch (error) {
console.error("Failed to fetch jobs:", error) console.error("Failed to fetch jobs:", error)
if (!isLoadMore) {
setJobs([])
setHasMore(false)
setJobsError(true)
}
} finally { } finally {
setLoading(false) setLoading(false)
setLoadingMore(false) setLoadingMore(false)
} }
}, [t]) }, [])
useEffect(() => { useEffect(() => {
fetchJobs(1) fetchJobs(1)
@ -72,6 +88,9 @@ export default function Home() {
fetchJobs(nextPage, true) fetchJobs(nextPage, true)
} }
const latestJobs = jobs.slice(0, 8)
const showEmptyState = !loading && !jobsError && jobs.length === 0
const scrollPrev = useCallback(() => { const scrollPrev = useCallback(() => {
if (emblaApi) emblaApi.scrollPrev() if (emblaApi) emblaApi.scrollPrev()
}, [emblaApi]) }, [emblaApi])
@ -165,8 +184,12 @@ export default function Home() {
<div className="overflow-hidden px-1 py-4" ref={emblaRef}> <div className="overflow-hidden px-1 py-4" ref={emblaRef}>
<div className="flex gap-6"> <div className="flex gap-6">
{loading ? ( {loading ? (
<div className="flex-[0_0_100%] text-center py-8">Carregando vagas...</div> <div className="flex-[0_0_100%] py-8 text-center text-muted-foreground">{t("jobs.loading")}</div>
) : jobs.slice(0, 8).map((job, index) => ( ) : jobsError ? (
<div className="flex-[0_0_100%] py-8 text-center text-sm text-red-600">{t("home.featuredJobs.error")}</div>
) : showEmptyState ? (
<div className="flex-[0_0_100%] py-8 text-center text-muted-foreground">{t("home.featuredJobs.empty")}</div>
) : latestJobs.map((job, index) => (
<div key={`latest-${job.id}-${index}`} className="flex-[0_0_100%] sm:flex-[0_0_50%] lg:flex-[0_0_50%] xl:flex-[0_0_33.333%] 2xl:flex-[0_0_25%] min-w-0 pb-1"> <div key={`latest-${job.id}-${index}`} className="flex-[0_0_100%] sm:flex-[0_0_50%] lg:flex-[0_0_50%] xl:flex-[0_0_33.333%] 2xl:flex-[0_0_25%] min-w-0 pb-1">
<JobCard job={job} /> <JobCard job={job} />
</div> </div>
@ -203,7 +226,11 @@ export default function Home() {
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6"> <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
{loading && page === 1 ? ( {loading && page === 1 ? (
<div className="col-span-full text-center py-12">Carregando vagas...</div> <div className="col-span-full py-12 text-center text-muted-foreground">{t("jobs.loading")}</div>
) : jobsError ? (
<div className="col-span-full py-12 text-center text-sm text-red-600">{t("home.moreJobs.error")}</div>
) : showEmptyState ? (
<div className="col-span-full py-12 text-center text-muted-foreground">{t("home.moreJobs.empty")}</div>
) : jobs.map((job, index) => ( ) : jobs.map((job, index) => (
<div key={`more-${job.id}-${index}`} className="pb-1"> <div key={`more-${job.id}-${index}`} className="pb-1">
<JobCard job={job} /> <JobCard job={job} />
@ -211,14 +238,14 @@ export default function Home() {
))} ))}
</div> </div>
{hasMore && ( {hasMore && !jobsError && jobs.length > 0 && (
<div className="mt-12 text-center"> <div className="mt-12 text-center">
<Button <Button
onClick={handleLoadMore} onClick={handleLoadMore}
disabled={loadingMore} disabled={loadingMore}
className="bg-orange-500 hover:bg-orange-600 text-white font-bold px-8 py-6 rounded-xl text-lg transition-all hover:scale-105 active:scale-95 shadow-lg" className="bg-orange-500 hover:bg-orange-600 text-white font-bold px-8 py-6 rounded-xl text-lg transition-all hover:scale-105 active:scale-95 shadow-lg"
> >
{loadingMore ? "Carregando..." : t("home.moreJobs.loadMore") || "Carregar Mais Vagas"} {loadingMore ? t("common.loading") : t("home.moreJobs.loadMore")}
</Button> </Button>
</div> </div>
)} )}

View file

@ -32,11 +32,19 @@ export function NotificationProvider({
useEffect(() => { useEffect(() => {
const loadNotifications = async () => { const loadNotifications = async () => {
const hasLocalSession =
typeof window !== "undefined" &&
Boolean(
localStorage.getItem("job-portal-auth") ||
localStorage.getItem("auth_token") ||
localStorage.getItem("token")
);
if (loading) { if (loading) {
return; return;
} }
if (!user) { if (!user || !hasLocalSession) {
setNotifications([]); setNotifications([]);
return; return;
} }

View file

@ -236,7 +236,9 @@
"yesterday": "Yesterday", "yesterday": "Yesterday",
"apply": "Apply now", "apply": "Apply now",
"viewJob": "View Job", "viewJob": "View Job",
"favorite": "Favorite" "favorite": "Favorite",
"empty": "No jobs available right now.",
"error": "Could not load the latest jobs right now."
}, },
"levels": { "levels": {
"junior": "Junior", "junior": "Junior",
@ -246,7 +248,9 @@
"moreJobs": { "moreJobs": {
"title": "More Jobs", "title": "More Jobs",
"viewAll": "View All Jobs", "viewAll": "View All Jobs",
"loadMore": "Load More Jobs" "loadMore": "Load More Jobs",
"empty": "No jobs available right now.",
"error": "Could not load more jobs right now."
}, },
"cta": { "cta": {
"badge": "Social Networks", "badge": "Social Networks",

View file

@ -236,7 +236,9 @@
"yesterday": "Ayer", "yesterday": "Ayer",
"apply": "Aplicar ahora", "apply": "Aplicar ahora",
"viewJob": "Ver Empleo", "viewJob": "Ver Empleo",
"favorite": "Favorito" "favorite": "Favorito",
"empty": "No hay empleos disponibles en este momento.",
"error": "No se pudieron cargar los últimos empleos ahora."
}, },
"levels": { "levels": {
"junior": "Junior", "junior": "Junior",
@ -246,7 +248,9 @@
"moreJobs": { "moreJobs": {
"title": "Más Empleos", "title": "Más Empleos",
"viewAll": "Ver Todos los Empleos", "viewAll": "Ver Todos los Empleos",
"loadMore": "Cargar Más Empleos" "loadMore": "Cargar Más Empleos",
"empty": "No hay empleos disponibles en este momento.",
"error": "No se pudieron cargar más empleos ahora."
}, },
"cta": { "cta": {
"badge": "Redes Sociales", "badge": "Redes Sociales",

View file

@ -276,7 +276,9 @@
"yesterday": "Ontem", "yesterday": "Ontem",
"apply": "Aplicar agora", "apply": "Aplicar agora",
"viewJob": "Ver Vaga", "viewJob": "Ver Vaga",
"favorite": "Favoritar" "favorite": "Favoritar",
"empty": "Nenhuma vaga encontrada no momento.",
"error": "Não foi possível carregar as últimas vagas agora."
}, },
"levels": { "levels": {
"junior": "Júnior", "junior": "Júnior",
@ -286,7 +288,9 @@
"moreJobs": { "moreJobs": {
"title": "Mais Vagas", "title": "Mais Vagas",
"viewAll": "Ver Todas Vagas", "viewAll": "Ver Todas Vagas",
"loadMore": "Carregar Mais Vagas" "loadMore": "Carregar Mais Vagas",
"empty": "Nenhuma vaga disponível no momento.",
"error": "Não foi possível carregar mais vagas agora."
}, },
"cta": { "cta": {
"badge": "Redes Sociais", "badge": "Redes Sociais",