classify web

This commit is contained in:
Husanjonazamov
2026-02-24 12:52:49 +05:00
commit 64af77101f
310 changed files with 45449 additions and 0 deletions

View 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>
);
}

View 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;

View 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;

View 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;

View 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,
};
}

View 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;

View 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;