fix(frontend): harden home jobs loading states
This commit is contained in:
parent
757429afe6
commit
a3febf2087
6 changed files with 63 additions and 15 deletions
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Reference in a new issue