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">
<DialogHeader>
<DialogTitle>{t('admin.jobs.details.title')}</DialogTitle>
<DialogDescription>{t('admin.jobs.details.description')}</DialogDescription>
</DialogHeader>
{selectedJob && (
<div className="grid gap-4 py-4">

View file

@ -19,6 +19,7 @@ export default function Home() {
const { t } = useTranslation()
const [jobs, setJobs] = useState<Job[]>([])
const [loading, setLoading] = useState(true)
const [jobsError, setJobsError] = useState(false)
const [page, setPage] = useState(1)
const [hasMore, setHasMore] = useState(true)
const [loadingMore, setLoadingMore] = useState(false)
@ -36,10 +37,18 @@ export default function Home() {
const fetchJobs = useCallback(async (pageNum: number, isLoadMore = false) => {
try {
if (isLoadMore) setLoadingMore(true)
else setLoading(true)
else {
setLoading(true)
setJobsError(false)
}
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) {
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 (newJobs.length < limit) {
setHasMore(false)
} else if (!isLoadMore) {
setHasMore(true)
}
}
} catch (error) {
console.error("Failed to fetch jobs:", error)
if (!isLoadMore) {
setJobs([])
setHasMore(false)
setJobsError(true)
}
} finally {
setLoading(false)
setLoadingMore(false)
}
}, [t])
}, [])
useEffect(() => {
fetchJobs(1)
@ -72,6 +88,9 @@ export default function Home() {
fetchJobs(nextPage, true)
}
const latestJobs = jobs.slice(0, 8)
const showEmptyState = !loading && !jobsError && jobs.length === 0
const scrollPrev = useCallback(() => {
if (emblaApi) emblaApi.scrollPrev()
}, [emblaApi])
@ -165,8 +184,12 @@ export default function Home() {
<div className="overflow-hidden px-1 py-4" ref={emblaRef}>
<div className="flex gap-6">
{loading ? (
<div className="flex-[0_0_100%] text-center py-8">Carregando vagas...</div>
) : jobs.slice(0, 8).map((job, index) => (
<div className="flex-[0_0_100%] py-8 text-center text-muted-foreground">{t("jobs.loading")}</div>
) : 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">
<JobCard job={job} />
</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">
{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) => (
<div key={`more-${job.id}-${index}`} className="pb-1">
<JobCard job={job} />
@ -211,14 +238,14 @@ export default function Home() {
))}
</div>
{hasMore && (
{hasMore && !jobsError && jobs.length > 0 && (
<div className="mt-12 text-center">
<Button
onClick={handleLoadMore}
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"
>
{loadingMore ? "Carregando..." : t("home.moreJobs.loadMore") || "Carregar Mais Vagas"}
{loadingMore ? t("common.loading") : t("home.moreJobs.loadMore")}
</Button>
</div>
)}

View file

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

View file

@ -236,7 +236,9 @@
"yesterday": "Yesterday",
"apply": "Apply now",
"viewJob": "View Job",
"favorite": "Favorite"
"favorite": "Favorite",
"empty": "No jobs available right now.",
"error": "Could not load the latest jobs right now."
},
"levels": {
"junior": "Junior",
@ -246,7 +248,9 @@
"moreJobs": {
"title": "More 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": {
"badge": "Social Networks",

View file

@ -236,7 +236,9 @@
"yesterday": "Ayer",
"apply": "Aplicar ahora",
"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": {
"junior": "Junior",
@ -246,7 +248,9 @@
"moreJobs": {
"title": "Más 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": {
"badge": "Redes Sociales",

View file

@ -276,7 +276,9 @@
"yesterday": "Ontem",
"apply": "Aplicar agora",
"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": {
"junior": "Júnior",
@ -286,7 +288,9 @@
"moreJobs": {
"title": "Mais 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": {
"badge": "Redes Sociais",