classify web
This commit is contained in:
48
components/Layout/Layout.jsx
Normal file
48
components/Layout/Layout.jsx
Normal file
@@ -0,0 +1,48 @@
|
||||
"use client";
|
||||
import Header from "../Common/Header";
|
||||
import Footer from "../Footer/Footer";
|
||||
import PushNotificationLayout from "./PushNotificationLayout";
|
||||
import Loading from "@/app/loading";
|
||||
import UnderMaintenance from "../../public/assets/something_went_wrong.svg";
|
||||
import { t } from "@/utils";
|
||||
import { useClientLayoutLogic } from "./useClientLayoutLogic";
|
||||
import CustomImage from "../Common/CustomImage";
|
||||
import ScrollToTopButton from "./ScrollToTopButton";
|
||||
|
||||
export default function Layout({ children }) {
|
||||
const { isLoading, isMaintenanceMode, isRedirectToLanding } =
|
||||
useClientLayoutLogic();
|
||||
|
||||
if (isLoading) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
if (isRedirectToLanding) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isMaintenanceMode) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center h-screen gap-2">
|
||||
<CustomImage
|
||||
src={UnderMaintenance}
|
||||
alt="Maintenance Mode"
|
||||
height={255}
|
||||
width={255}
|
||||
/>
|
||||
<p className="text-center max-w-[40%]">{t("underMaintenance")}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<PushNotificationLayout>
|
||||
<div className="flex flex-col min-h-screen">
|
||||
<Header />
|
||||
<div className="flex-1">{children}</div>
|
||||
<ScrollToTopButton />
|
||||
<Footer />
|
||||
</div>
|
||||
</PushNotificationLayout>
|
||||
);
|
||||
}
|
||||
101
components/Layout/PushNotificationLayout.jsx
Normal file
101
components/Layout/PushNotificationLayout.jsx
Normal file
@@ -0,0 +1,101 @@
|
||||
"use client";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import "firebase/messaging";
|
||||
import FirebaseData from "../../utils/Firebase";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { setNotification } from "@/redux/reducer/globalStateSlice";
|
||||
import { useNavigate } from "../Common/useNavigate";
|
||||
import { getIsLoggedIn } from "@/redux/reducer/authSlice";
|
||||
|
||||
const PushNotificationLayout = ({ children }) => {
|
||||
const dispatch = useDispatch();
|
||||
const [fcmToken, setFcmToken] = useState("");
|
||||
const { fetchToken, onMessageListener } = FirebaseData();
|
||||
const { navigate } = useNavigate();
|
||||
const isLoggedIn = useSelector(getIsLoggedIn);
|
||||
const unsubscribeRef = useRef(null);
|
||||
|
||||
const handleFetchToken = async () => {
|
||||
await fetchToken(setFcmToken);
|
||||
};
|
||||
|
||||
// Fetch token when user logs in
|
||||
useEffect(() => {
|
||||
handleFetchToken();
|
||||
}, []);
|
||||
|
||||
// Set up message listener when logged in, clean up when logged out
|
||||
useEffect(() => {
|
||||
if (!isLoggedIn) {
|
||||
// Clean up listener when user logs out
|
||||
if (unsubscribeRef.current) {
|
||||
unsubscribeRef.current();
|
||||
unsubscribeRef.current = null;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Set up listener when user logs in
|
||||
const setupListener = async () => {
|
||||
try {
|
||||
unsubscribeRef.current = await onMessageListener((payload) => {
|
||||
if (payload && payload.data) {
|
||||
dispatch(setNotification(payload.data));
|
||||
if (Notification.permission === "granted") {
|
||||
const notif = new Notification(payload.notification.title, {
|
||||
body: payload.notification.body,
|
||||
});
|
||||
const tab =
|
||||
payload.data?.user_type === "Seller" ? "buying" : "selling";
|
||||
|
||||
notif.onclick = () => {
|
||||
if (
|
||||
payload.data.type === "chat" ||
|
||||
payload.data.type === "offer"
|
||||
) {
|
||||
navigate(
|
||||
`/chat?activeTab=${tab}&chatid=${payload.data?.item_offer_id}`
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Error handling foreground notification:", err);
|
||||
}
|
||||
};
|
||||
|
||||
setupListener();
|
||||
|
||||
// Cleanup on unmount or logout
|
||||
return () => {
|
||||
if (unsubscribeRef.current) {
|
||||
unsubscribeRef.current();
|
||||
unsubscribeRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [isLoggedIn, dispatch, navigate, onMessageListener]);
|
||||
|
||||
useEffect(() => {
|
||||
if (fcmToken) {
|
||||
if ("serviceWorker" in navigator) {
|
||||
navigator.serviceWorker
|
||||
.register("/firebase-messaging-sw.js")
|
||||
.then((registration) => {
|
||||
console.log(
|
||||
"Service Worker registration successful with scope: ",
|
||||
registration.scope
|
||||
);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log("Service Worker registration failed: ", err);
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [fcmToken]);
|
||||
|
||||
return children;
|
||||
};
|
||||
|
||||
export default PushNotificationLayout;
|
||||
41
components/Layout/ScrollToTopButton.jsx
Normal file
41
components/Layout/ScrollToTopButton.jsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useEffect, useState } from "react";
|
||||
import { IoIosArrowUp } from "react-icons/io";
|
||||
|
||||
const ScrollToTopButton = () => {
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const toggleVisibility = () => {
|
||||
if (window.pageYOffset > 300) {
|
||||
setIsVisible(true);
|
||||
} else {
|
||||
setIsVisible(false);
|
||||
}
|
||||
};
|
||||
window.addEventListener("scroll", toggleVisibility);
|
||||
return () => {
|
||||
window.removeEventListener("scroll", toggleVisibility);
|
||||
};
|
||||
}, []);
|
||||
const scrollToTop = () => {
|
||||
window.scrollTo({
|
||||
top: 0,
|
||||
behavior: "smooth",
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={scrollToTop}
|
||||
className={cn(
|
||||
"fixed bottom-7 right-7 bg-primary text-white rounded z-[1000] p-2 flex items-center justify-center size-12",
|
||||
isVisible ? "flex" : "hidden"
|
||||
)}
|
||||
>
|
||||
<IoIosArrowUp size={22} />
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export default ScrollToTopButton;
|
||||
14
components/Layout/StructuredData.jsx
Normal file
14
components/Layout/StructuredData.jsx
Normal file
@@ -0,0 +1,14 @@
|
||||
const StructuredData = ({ data }) => {
|
||||
if (!data) return null;
|
||||
|
||||
return (
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: JSON.stringify(data).replace(/</g, "\\u003c"),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default StructuredData;
|
||||
100
components/Layout/useClientLayoutLogic.jsx
Normal file
100
components/Layout/useClientLayoutLogic.jsx
Normal file
@@ -0,0 +1,100 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { settingsApi } from "@/utils/api";
|
||||
import {
|
||||
settingsSucess,
|
||||
getIsMaintenanceMode,
|
||||
} from "@/redux/reducer/settingSlice";
|
||||
import {
|
||||
getKilometerRange,
|
||||
setKilometerRange,
|
||||
setIsBrowserSupported,
|
||||
} from "@/redux/reducer/locationSlice";
|
||||
import { getIsVisitedLandingPage } from "@/redux/reducer/globalStateSlice";
|
||||
import { getCurrentLangCode, getIsRtl } from "@/redux/reducer/languageSlice";
|
||||
import {
|
||||
getHasFetchedSystemSettings,
|
||||
setHasFetchedSystemSettings,
|
||||
} from "@/utils/getFetcherStatus";
|
||||
import { useNavigate } from "../Common/useNavigate";
|
||||
|
||||
export function useClientLayoutLogic() {
|
||||
const dispatch = useDispatch();
|
||||
const { navigate } = useNavigate();
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const currentLangCode = useSelector(getCurrentLangCode);
|
||||
const isMaintenanceMode = useSelector(getIsMaintenanceMode);
|
||||
const isRtl = useSelector(getIsRtl);
|
||||
const appliedRange = useSelector(getKilometerRange);
|
||||
const isVisitedLandingPage = useSelector(getIsVisitedLandingPage);
|
||||
const [isRedirectToLanding, setIsRedirectToLanding] = useState(false);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const getSystemSettings = async () => {
|
||||
if (getHasFetchedSystemSettings()) {
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// Get settings from API
|
||||
const response = await settingsApi.getSettings();
|
||||
const data = response?.data;
|
||||
dispatch(settingsSucess({ data }));
|
||||
|
||||
// Set kilometer range from settings API
|
||||
const min = Number(data?.data?.min_length);
|
||||
const max = Number(data?.data?.max_length);
|
||||
if (appliedRange < min) dispatch(setKilometerRange(min));
|
||||
else if (appliedRange > max) dispatch(setKilometerRange(max));
|
||||
|
||||
// Set primary color from settings API
|
||||
document.documentElement.style.setProperty(
|
||||
"--primary",
|
||||
data?.data?.web_theme_color
|
||||
);
|
||||
|
||||
// Set favicon from settings API
|
||||
if (data?.data?.favicon_icon) {
|
||||
const favicon =
|
||||
document.querySelector('link[rel="icon"]') ||
|
||||
document.createElement("link");
|
||||
favicon.rel = "icon";
|
||||
favicon.href = data.data.favicon_icon;
|
||||
if (!document.querySelector('link[rel="icon"]')) {
|
||||
document.head.appendChild(favicon);
|
||||
}
|
||||
}
|
||||
|
||||
setHasFetchedSystemSettings(true);
|
||||
// Check if landing page is enabled and redirect to landing page if not visited
|
||||
const showLandingPage = Number(data?.data?.show_landing_page) === 1;
|
||||
if (showLandingPage && !isVisitedLandingPage) {
|
||||
setIsRedirectToLanding(true);
|
||||
navigate("/landing");
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching settings:", error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
getSystemSettings();
|
||||
|
||||
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
|
||||
if (isSafari) dispatch(setIsBrowserSupported(false));
|
||||
}, [currentLangCode]);
|
||||
|
||||
// Set direction of the document
|
||||
useEffect(() => {
|
||||
document.documentElement.dir = isRtl ? "rtl" : "ltr";
|
||||
}, [isRtl]);
|
||||
|
||||
return {
|
||||
isLoading,
|
||||
isMaintenanceMode,
|
||||
isRedirectToLanding,
|
||||
};
|
||||
}
|
||||
72
components/Layout/useGetCategories.jsx
Normal file
72
components/Layout/useGetCategories.jsx
Normal file
@@ -0,0 +1,72 @@
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import {
|
||||
CategoryData,
|
||||
getCatCurrentPage,
|
||||
getCatLastPage,
|
||||
getIsCatLoading,
|
||||
getIsCatLoadMore,
|
||||
setCatCurrentPage,
|
||||
setCateData,
|
||||
setCatLastPage,
|
||||
setIsCatLoading,
|
||||
setIsCatLoadMore,
|
||||
} from "@/redux/reducer/categorySlice";
|
||||
import { categoryApi } from "@/utils/api"; // assume you have this
|
||||
import { useCallback } from "react";
|
||||
import {
|
||||
getHasFetchedCategories,
|
||||
setHasFetchedCategories,
|
||||
} from "@/utils/getFetcherStatus";
|
||||
|
||||
const useGetCategories = () => {
|
||||
const dispatch = useDispatch();
|
||||
const cateData = useSelector(CategoryData);
|
||||
const isCatLoading = useSelector(getIsCatLoading);
|
||||
const isCatLoadMore = useSelector(getIsCatLoadMore);
|
||||
const catLastPage = useSelector(getCatLastPage);
|
||||
const catCurrentPage = useSelector(getCatCurrentPage);
|
||||
|
||||
const getCategories = useCallback(
|
||||
async (page = 1) => {
|
||||
if (page === 1 && getHasFetchedCategories()) {
|
||||
return;
|
||||
}
|
||||
if (page === 1) {
|
||||
dispatch(setIsCatLoading(true));
|
||||
} else {
|
||||
dispatch(setIsCatLoadMore(true));
|
||||
}
|
||||
try {
|
||||
const res = await categoryApi.getCategory({ page });
|
||||
if (res?.data?.error === false) {
|
||||
const data = res?.data?.data?.data;
|
||||
if (page === 1) {
|
||||
dispatch(setCateData(data));
|
||||
} else {
|
||||
dispatch(setCateData([...cateData, ...data]));
|
||||
}
|
||||
dispatch(setCatCurrentPage(res?.data?.data?.current_page));
|
||||
dispatch(setCatLastPage(res?.data?.data?.last_page));
|
||||
setHasFetchedCategories(true);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
} finally {
|
||||
dispatch(setIsCatLoading(false));
|
||||
dispatch(setIsCatLoadMore(false));
|
||||
}
|
||||
},
|
||||
[cateData, dispatch]
|
||||
);
|
||||
|
||||
return {
|
||||
getCategories,
|
||||
isCatLoading,
|
||||
cateData,
|
||||
isCatLoadMore,
|
||||
catLastPage,
|
||||
catCurrentPage,
|
||||
};
|
||||
};
|
||||
|
||||
export default useGetCategories;
|
||||
87
components/Layout/useGetLocation.jsx
Normal file
87
components/Layout/useGetLocation.jsx
Normal file
@@ -0,0 +1,87 @@
|
||||
import { getCurrentLangCode } from "@/redux/reducer/languageSlice";
|
||||
import { getIsPaidApi } from "@/redux/reducer/settingSlice";
|
||||
import { getLocationApi } from "@/utils/api";
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
const useGetLocation = () => {
|
||||
const IsPaidApi = useSelector(getIsPaidApi);
|
||||
const currentLangCode = useSelector(getCurrentLangCode);
|
||||
|
||||
const fetchLocationData = async (pos) => {
|
||||
const { lat, lng } = pos;
|
||||
|
||||
const response = await getLocationApi.getLocation({
|
||||
lat,
|
||||
lng,
|
||||
lang: IsPaidApi ? "en" : currentLangCode,
|
||||
});
|
||||
|
||||
if (response?.data?.error !== false) {
|
||||
throw new Error("Location fetch failed");
|
||||
}
|
||||
|
||||
/* ================= GOOGLE PLACES (PAID API) ================= */
|
||||
if (IsPaidApi) {
|
||||
let city = "";
|
||||
let state = "";
|
||||
let country = "";
|
||||
|
||||
const results = response?.data?.data?.results || [];
|
||||
|
||||
results.forEach((result) => {
|
||||
const getComponent = (type) =>
|
||||
result.address_components.find((c) => c.types.includes(type))
|
||||
?.long_name || "";
|
||||
|
||||
if (!city) city = getComponent("locality");
|
||||
if (!state) state = getComponent("administrative_area_level_1");
|
||||
if (!country) country = getComponent("country");
|
||||
});
|
||||
|
||||
return {
|
||||
lat,
|
||||
long: lng,
|
||||
city,
|
||||
state,
|
||||
country,
|
||||
formattedAddress: [city, state, country].filter(Boolean).join(", "),
|
||||
};
|
||||
}
|
||||
|
||||
/* ================= INTERNAL LOCATION API ================= */
|
||||
const r = response?.data?.data;
|
||||
|
||||
const formattedAddress = [r?.area, r?.city, r?.state, r?.country]
|
||||
.filter(Boolean)
|
||||
.join(", ");
|
||||
|
||||
const address_translated = [
|
||||
r?.area_translation,
|
||||
r?.city_translation,
|
||||
r?.state_translation,
|
||||
r?.country_translation,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(", ");
|
||||
|
||||
return {
|
||||
lat: r?.latitude,
|
||||
long: r?.longitude,
|
||||
city: r?.city || "",
|
||||
state: r?.state || "",
|
||||
country: r?.country || "",
|
||||
area: r?.area || "",
|
||||
areaId: r?.area_id || "",
|
||||
|
||||
// English (API / backend)
|
||||
formattedAddress,
|
||||
|
||||
// Translated (UI)
|
||||
address_translated,
|
||||
};
|
||||
};
|
||||
|
||||
return { fetchLocationData };
|
||||
};
|
||||
|
||||
export default useGetLocation;
|
||||
Reference in New Issue
Block a user