Commit 91a2bc40 authored by Lê Đức Huy's avatar Lê Đức Huy

feat: implement header, admin views, sidebar, and core api query configurations

parent 1334e92b
// Core // Core
import { AxiosError, isAxiosError } from 'axios' import { AxiosError, isAxiosError } from 'axios'
import { QueryClient } from '@tanstack/react-query' import { QueryClient } from '@tanstack/react-query'
// App // App
// import router from '@/router' // import router from '@/router'
import { handleAdminUnauthorized } from '@/lib/auth/admin-auth' import { handleAdminUnauthorized } from '@/lib/auth/admin-auth'
// import useProfileStore from '@stores/profile' // import useProfileStore from '@stores/profile'
import { QueryData } from '@/lib/types/base-api' import { QueryData } from '@/lib/types/base-api'
// import { BASE_PATHS } from '@/constants/path' // import { BASE_PATHS } from '@/constants/path'
// Constants // Constants
const RETRY_COUNT = 3 const RETRY_COUNT = 3
const EXPIRED_TOKEN_ERROR = 401 const EXPIRED_TOKEN_ERROR = 401
const DENIED_PERMISSION_ERROR = 403 const DENIED_PERMISSION_ERROR = 403
const INTERNAL_SERVER_ERROR = 500 const INTERNAL_SERVER_ERROR = 500
const API_QUERY_STALE_TIME = 2 * 60 * 1000 const API_QUERY_STALE_TIME = 2 * 60 * 1000
const API_QUERY_GC_TIME = 10 * 60 * 1000 const API_QUERY_GC_TIME = 10 * 60 * 1000
// Utils // Utils
// Handle check base retry logical // Handle check base retry logical
...@@ -27,8 +27,10 @@ const handleCheckBaseRetryLogical = (failureCount: number, error: Error) => { ...@@ -27,8 +27,10 @@ const handleCheckBaseRetryLogical = (failureCount: number, error: Error) => {
// Expired token error // Expired token error
if (error.response?.status === EXPIRED_TOKEN_ERROR) { if (error.response?.status === EXPIRED_TOKEN_ERROR) {
handleUnAuthorizationError() if (typeof window !== "undefined" && window.location.pathname.startsWith("/admin")) {
return false handleUnAuthorizationError();
}
return false;
} }
// Denied permission error // Denied permission error
...@@ -42,15 +44,15 @@ const handleCheckBaseRetryLogical = (failureCount: number, error: Error) => { ...@@ -42,15 +44,15 @@ const handleCheckBaseRetryLogical = (failureCount: number, error: Error) => {
} }
// Handle un authorization error // Handle un authorization error
const handleUnAuthorizationError = () => { const handleUnAuthorizationError = () => {
void handleAdminUnauthorized() void handleAdminUnauthorized()
// useProfileStore.getState().resetStore() // useProfileStore.getState().resetStore()
// const languageAwarePath = addLanguageToPath({ // const languageAwarePath = addLanguageToPath({
// path: BASE_PATHS.authSignIn // path: BASE_PATHS.authSignIn
// }) // })
// router.navigate('') // router.navigate('')
} }
// Handle delay value // Handle delay value
const handleDelayRetry = (failureCount: number) => failureCount * 1000 + Math.random() * 1000 const handleDelayRetry = (failureCount: number) => failureCount * 1000 + Math.random() * 1000
...@@ -58,13 +60,13 @@ const handleDelayRetry = (failureCount: number) => failureCount * 1000 + Math.ra ...@@ -58,13 +60,13 @@ const handleDelayRetry = (failureCount: number) => failureCount * 1000 + Math.ra
// Query client // Query client
export const queryClient = new QueryClient({ export const queryClient = new QueryClient({
defaultOptions: { defaultOptions: {
queries: { queries: {
staleTime: API_QUERY_STALE_TIME, staleTime: API_QUERY_STALE_TIME,
gcTime: API_QUERY_GC_TIME, gcTime: API_QUERY_GC_TIME,
refetchOnWindowFocus: false, refetchOnWindowFocus: false,
refetchOnMount: false, refetchOnMount: false,
refetchOnReconnect: false, refetchOnReconnect: false,
placeholderData: (previousData: unknown) => previousData, placeholderData: (previousData: unknown) => previousData,
retry(failureCount, error) { retry(failureCount, error) {
if (!handleCheckBaseRetryLogical(failureCount, error)) return false if (!handleCheckBaseRetryLogical(failureCount, error)) return false
......
'use client'; "use client";
import ImageNext from "@/components/shared/image-next"; import ImageNext from "@/components/shared/image-next";
import { Swiper, SwiperSlide } from "swiper/react"; import { Swiper, SwiperSlide } from "swiper/react";
...@@ -7,40 +7,98 @@ import { Swiper as SwiperType } from "swiper/types"; ...@@ -7,40 +7,98 @@ import { Swiper as SwiperType } from "swiper/types";
import { useRef } from "react"; import { useRef } from "react";
import "swiper/css"; import "swiper/css";
import { useGetBanner } from "@/api/endpoints/banner";
import { useQuery } from "@tanstack/react-query";
import { fetchCmsFileById, resolveCmsFileUrl } from "@/lib/api/files";
import { Skeleton } from "@/components/ui/skeleton";
type ApiEnvelope<T> = {
responseData?: T;
data?: {
responseData?: T;
};
};
const getEnvelopeData = <T,>(payload?: ApiEnvelope<T>) =>
payload?.responseData ?? payload?.data?.responseData;
function BannerSlideItem({ fileId, alt }: { fileId: string; alt: string }) {
const { data: file, isPending } = useQuery({
queryKey: ["file", fileId],
queryFn: () => fetchCmsFileById(fileId),
enabled: !!fileId,
});
if (isPending) {
return (
<Skeleton className="w-full h-[200px] sm:h-[300px] md:h-[400px] lg:h-[500px]" />
);
}
const url = file ? resolveCmsFileUrl(file.path) : "/img-error.png";
return (
<ImageNext
src={url}
alt={alt}
width={2560}
height={720}
sizes="100vw"
className="w-full h-[200px] sm:h-[300px] md:h-[400px] lg:h-[500px] object-cover"
/>
);
}
const Banner = () => { const Banner = () => {
const swiperRef = useRef<SwiperType | null>(null); const swiperRef = useRef<SwiperType | null>(null);
const { data: bannerData } = useGetBanner({
filters: "status@=ACTIVE",
sortField: "display_order",
sortOrder: "asc",
});
const pageData = getEnvelopeData<{ rows?: any[] }>(bannerData);
const rows = pageData?.rows ?? [];
if (!rows || rows.length === 0) {
return (
<div className="w-full h-[200px] sm:h-[300px] md:h-[400px] lg:h-[500px] bg-slate-100 flex items-center justify-center">
<Skeleton className="w-full h-full" />
</div>
);
}
return ( return (
<Swiper <Swiper
modules={[Autoplay]} modules={[Autoplay]}
autoplay={{ delay: 4000, disableOnInteraction: false }} autoplay={{ delay: 4000, disableOnInteraction: false }}
loop loop={rows.length > 1}
slidesPerView={1} slidesPerView={1}
onSwiper={(s) => (swiperRef.current = s)} onSwiper={(s) => (swiperRef.current = s)}
className="w-full overflow-hidden" className="w-full overflow-hidden"
> >
<SwiperSlide> {rows.map((row: any) => (
<ImageNext <SwiperSlide key={row.id}>
src="https://vcci-hcm.org.vn/wp-content/uploads/2025/10/1.1.-Hero-Banner-CEO-2025-Bi-Sai-Nam-2025-Nhe-2560x720-Px.jpg.webp" {row.file_id ? (
alt="Banner" <BannerSlideItem
width={2560} fileId={row.file_id}
height={720} alt={row.banner_name || "Banner"}
sizes="100vw" />
className="w-full h-[200px] sm:h-[300px] md:h-[400px] lg:h-[500px] object-cover" ) : (
/> <ImageNext
</SwiperSlide> src="/img-error.png"
<SwiperSlide> alt={row.banner_name || "Banner"}
<ImageNext width={2560}
src="https://vcci-hcm.org.vn/wp-content/uploads/2022/07/Landscape-HCM_3-01.png" height={720}
alt="Banner" sizes="100vw"
width={2560} className="w-full h-[200px] sm:h-[300px] md:h-[400px] lg:h-[500px] object-cover"
height={720} />
sizes="100vw" )}
className="w-full h-[200px] sm:h-[300px] md:h-[400px] lg:h-[500px] object-cover" </SwiperSlide>
/> ))}
</SwiperSlide>
</Swiper> </Swiper>
); );
} };
export default Banner; export default Banner;
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment