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,165 @@
import ProductCard from "@/components/Common/ProductCard";
import NoData from "@/components/EmptyStates/NoData";
import AllItemsSkeleton from "@/components/PagesComponent/Home/AllItemsSkeleton";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import { resetBreadcrumb } from "@/redux/reducer/breadCrumbSlice";
import { CurrentLanguageData } from "@/redux/reducer/languageSlice";
import { t } from "@/utils";
import { allItemApi } from "@/utils/api";
import { Info, X } from "lucide-react";
import { useEffect, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
const AllItems = ({ cityData, KmRange }) => {
const dispatch = useDispatch();
const CurrentLanguage = useSelector(CurrentLanguageData);
const [AllItem, setAllItem] = useState([]);
const [hasMore, setHasMore] = useState(true);
const [currentPage, setCurrentPage] = useState(1);
const [isLoading, setIsLoading] = useState(false);
const [isLoadMore, setIsLoadMore] = useState(false);
// State to track if we should show location alert
const [locationAlertMessage, setLocationAlertMessage] = useState("");
const getAllItemData = async (page) => {
if (page === 1) {
setIsLoading(true);
}
try {
const params = {
page,
current_page: "home",
};
if (Number(KmRange) > 0 && (cityData?.areaId || cityData?.city)) {
// Add location-based parameters for non-demo mode
params.radius = KmRange;
params.latitude = cityData.lat;
params.longitude = cityData.long;
} else {
// Add location hierarchy parameters for non-demo mode
if (cityData?.areaId) {
params.area_id = cityData.areaId;
} else if (cityData?.city) {
params.city = cityData.city;
} else if (cityData?.state) {
params.state = cityData.state;
} else if (cityData?.country) {
params.country = cityData.country;
}
}
const response = await allItemApi.getItems(params);
if (response.data?.error === true) {
throw new Error(response.data?.message);
}
const apiMessage = response.data.message;
// Check if message indicates no items in selected location
const isNoItemsInLocation = apiMessage
?.toLowerCase()
.includes("no ads found");
// Show alert only if there are items but from different location
if (isNoItemsInLocation && response?.data?.data?.data?.length > 0) {
setLocationAlertMessage(apiMessage);
} else {
setLocationAlertMessage("");
}
if (response?.data?.data?.data?.length > 0) {
const data = response?.data?.data?.data;
if (page === 1) {
setAllItem(data);
} else {
setAllItem((prevData) => [...prevData, ...data]);
}
const currentPage = response?.data?.data?.current_page;
const lastPage = response?.data?.data?.last_page;
setHasMore(currentPage < lastPage);
setCurrentPage(currentPage);
} else {
setAllItem([]);
}
} catch (error) {
console.error("Error:", error);
} finally {
setIsLoading(false);
setIsLoadMore(false);
}
};
useEffect(() => {
getAllItemData(1);
}, [cityData.lat, cityData.long, KmRange, CurrentLanguage?.id]);
const handleLoadMore = () => {
setIsLoadMore(true);
getAllItemData(currentPage + 1);
};
useEffect(() => {
// reset breadcrumb path when in home page
dispatch(resetBreadcrumb());
}, []);
const handleLikeAllData = (id) => {
const updatedItems = AllItem.map((item) => {
if (item.id === id) {
return { ...item, is_liked: !item.is_liked };
}
return item;
});
setAllItem(updatedItems);
};
return (
<section className="container mt-12">
<h5 className="text-xl sm:text-2xl font-medium">
{t("allAdvertisements")}
</h5>
{/* Location Alert - shows when items are from different location */}
{locationAlertMessage && AllItem.length > 0 && (
<Alert variant="warning" className="mt-3">
<Info className="size-4" />
<AlertTitle>{locationAlertMessage}</AlertTitle>
<AlertDescription className="sr-only"></AlertDescription>
</Alert>
)}
<div className="grid grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-3 sm:gap-6 mt-6">
{isLoading ? (
<AllItemsSkeleton />
) : AllItem && AllItem.length > 0 ? (
AllItem?.map((item) => (
<ProductCard
key={item?.id}
item={item}
handleLike={handleLikeAllData}
/>
))
) : (
<div className="col-span-full">
<NoData name={t("advertisement")} />
</div>
)}
</div>
{AllItem && AllItem.length > 0 && hasMore && (
<div className="text-center mt-6">
<Button
variant="outline"
className="text-sm sm:text-base text-primary w-[256px]"
disabled={isLoading || isLoadMore}
onClick={handleLoadMore}
>
{isLoadMore ? t("loading") : t("loadMore")}
</Button>
</div>
)}
</section>
);
};
export default AllItems;

View File

@@ -0,0 +1,14 @@
import { useId } from "react";
import ProductCardSkeleton from "../../Common/ProductCardSkeleton";
const AllItemsSkeleton = () => {
return (
<>
{Array.from({ length: 8 }).map(() => (
<ProductCardSkeleton key={useId()} />
))}
</>
);
};
export default AllItemsSkeleton;

View File

@@ -0,0 +1,62 @@
"use client";
import { t } from "@/utils";
import CustomLink from "@/components/Common/CustomLink";
import ProductCard from "@/components/Common/ProductCard";
import { Fragment } from "react";
const FeaturedSections = ({ featuredData, setFeaturedData, allEmpty }) => {
const handleLike = (id) => {
const updatedData = featuredData.map((section) => {
const updatedSectionData = section.section_data.map((item) => {
if (item.id === id) {
return { ...item, is_liked: !item.is_liked };
}
return item;
});
return { ...section, section_data: updatedSectionData };
});
setFeaturedData(updatedData);
};
return (
featuredData &&
featuredData.length > 0 &&
!allEmpty && (
<section className="container">
{featuredData.map(
(ele) =>
ele?.section_data.length > 0 && (
<Fragment key={ele?.id}>
<div className="space-between gap-2 mt-12">
<h5 className="text-xl sm:text-2xl font-medium">
{ele?.translated_name || ele?.title}
</h5>
{ele?.section_data.length > 4 && (
<CustomLink
href={`/ads?featured_section=${ele?.slug}`}
className="text-sm sm:text-base font-medium whitespace-nowrap"
prefetch={false}
>
{t("viewAll")}
</CustomLink>
)}
</div>
<div className="grid grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-3 sm:gap-6 mt-6">
{ele?.section_data.slice(0, 4).map((data) => (
<ProductCard
key={data?.id}
item={data}
handleLike={handleLike}
/>
))}
</div>
</Fragment>
)
)}
</section>
)
);
};
export default FeaturedSections;

View File

@@ -0,0 +1,20 @@
import { Skeleton } from "../../ui/skeleton";
import ProductCardSkeleton from "../../Common/ProductCardSkeleton";
const FeaturedSectionsSkeleton = () => {
return (
<div className="container">
<div className="space-between gap-2 mt-12">
<Skeleton className="w-1/6 h-4" />
<Skeleton className="w-1/12 h-4" />
</div>
<div className="grid grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-3 sm:gap-6 mt-6">
{Array.from({ length: 4 }).map((_, index) => (
<ProductCardSkeleton key={index} />
))}
</div>
</div>
);
};
export default FeaturedSectionsSkeleton;

View File

@@ -0,0 +1,300 @@
import {
NavigationMenu,
NavigationMenuContent,
NavigationMenuItem,
NavigationMenuLink,
NavigationMenuList,
NavigationMenuTrigger,
navigationMenuTriggerStyle,
} from "@/components/ui/navigation-menu";
import { t } from "@/utils";
import CustomLink from "@/components/Common/CustomLink";
import { useEffect, useRef, useState } from "react";
import { IoIosMore } from "react-icons/io";
import CustomImage from "@/components/Common/CustomImage";
import { useNavigate } from "@/components/Common/useNavigate";
import { usePathname, useSearchParams } from "next/navigation";
const HeaderCategories = ({ cateData }) => {
const containerRef = useRef(null);
const measureRef = useRef(null);
const pathname = usePathname();
const searchParams = useSearchParams();
const { navigate } = useNavigate();
const [fitCategoriesCount, setFitCategoriesCount] = useState(3);
useEffect(() => {
const calculateFit = () => {
if (!containerRef.current || !measureRef.current) return;
const containerWidth = containerRef.current.offsetWidth;
const otherWidth = 80; //approx width of other option
const availableWidth = containerWidth - otherWidth;
const items = Array.from(measureRef.current.children);
let totalWidth = 0;
let visible = 0;
for (const item of items) {
const width = item.getBoundingClientRect().width + 48; // padding/gap buffer
if (totalWidth + width > availableWidth) break;
totalWidth += width;
visible++;
}
setFitCategoriesCount(visible);
};
const resizeObserver = new ResizeObserver(calculateFit);
if (containerRef.current) {
resizeObserver.observe(containerRef.current);
}
return () => resizeObserver.disconnect();
}, [cateData]);
// Helper function to build URL with category while preserving existing search params
const buildCategoryUrl = (categorySlug) => {
if (pathname.startsWith("/ads")) {
// Preserve existing search params and update category
const newSearchParams = new URLSearchParams(searchParams.toString());
newSearchParams.delete("lang");
newSearchParams.set("category", categorySlug);
return `/ads?${newSearchParams.toString()}`;
} else {
// Not on ads page, just add category
return `/ads?category=${categorySlug}`;
}
};
const handleCategoryClick = (slug) => {
if (pathname.startsWith("/ads")) {
const newSearchParams = new URLSearchParams(searchParams.toString());
newSearchParams.set("category", slug);
const newUrl = `/ads?${newSearchParams.toString()}`;
window.history.pushState(null, "", newUrl);
} else {
navigate(`/ads?category=${slug}`);
}
};
const handleOtherCategoryClick = () => {
if (pathname.startsWith("/ads")) {
const newSearchParams = new URLSearchParams(searchParams.toString());
newSearchParams.delete("category");
const newUrl = `/ads?${newSearchParams.toString()}`;
window.history.pushState(null, "", newUrl);
} else {
navigate(`/ads`);
}
};
return (
<div className="hidden lg:block py-1.5 border-b">
<div className="container" ref={containerRef}>
{/* Hidden measurement row */}
<div
ref={measureRef}
className="absolute opacity-0 pointer-events-none flex"
style={{ position: "fixed", top: -9999, left: -9999 }}
>
{cateData.map((category) => (
<div key={category.id} className="px-2">
<span className="whitespace-nowrap font-medium text-sm">
{category.translated_name}
</span>
</div>
))}
</div>
<NavigationMenu>
<NavigationMenuList className="rtl:flex-row-reverse">
{cateData?.slice(0, fitCategoriesCount)?.map((category) => (
<NavigationMenuItem key={category.id}>
{category.subcategories_count > 0 ? (
<>
<NavigationMenuTrigger
onClick={() => handleCategoryClick(category.slug)}
>
{category.translated_name}
</NavigationMenuTrigger>
<NavigationMenuContent className="rtl:[direction:rtl]">
<NavigationMenuLink asChild>
<div
style={{
width: containerRef?.current?.offsetWidth - 32,
}}
className="flex overflow-x-auto"
>
<div className="w-[20%] p-4 bg-muted">
<div className="flex gap-1">
<CustomImage
src={category?.image}
alt={category?.translated_name}
width={22}
height={22}
className="w-22 h-auto aspect-square"
/>
<p className="font-bold">
{category?.translated_name}
</p>
</div>
</div>
<div className="w-[80%] p-4">
<div className="flex flex-col flex-wrap w-min gap-8 h-[30vh] max-h-[30vh]">
{/* <CustomLink
href={`/ads?category=${category.slug}`}
className="font-semibold whitespace-nowrap text-sm hover:text-primary"
>
{t("seeAllIn")} {category.translated_name}
</CustomLink> */}
{category.subcategories.map((subcategory) => (
<div key={subcategory.id}>
<CustomLink
href={buildCategoryUrl(subcategory.slug)}
className="font-semibold whitespace-nowrap text-sm hover:text-primary"
>
{subcategory.translated_name}
</CustomLink>
{subcategory.subcategories_count > 0 && (
<ul className="flex flex-col gap-2 mt-2">
{subcategory?.subcategories
?.slice(0, 5)
.map((nestedSubcategory) => (
<li
key={nestedSubcategory?.id}
className="text-xs"
>
<CustomLink
href={buildCategoryUrl(
nestedSubcategory?.slug
)}
className="hover:text-primary whitespace-nowrap"
>
{
nestedSubcategory?.translated_name
}
</CustomLink>
</li>
))}
{subcategory.subcategories.length > 5 && (
<li className="text-xs">
<CustomLink
href={buildCategoryUrl(
subcategory.slug
)}
className="hover:text-primary"
>
{t("viewAll")}
</CustomLink>
</li>
)}
</ul>
)}
</div>
))}
</div>
</div>
</div>
</NavigationMenuLink>
</NavigationMenuContent>
</>
) : (
<NavigationMenuLink
className={navigationMenuTriggerStyle()}
href={buildCategoryUrl(category?.slug)}
asChild
>
<CustomLink href={buildCategoryUrl(category?.slug)}>
{category.translated_name}
</CustomLink>
</NavigationMenuLink>
)}
</NavigationMenuItem>
))}
{cateData && cateData.length > fitCategoriesCount && (
<NavigationMenuItem>
<NavigationMenuTrigger onClick={handleOtherCategoryClick}>
{t("other")}
</NavigationMenuTrigger>
<NavigationMenuContent className="rtl:[direction:rtl]">
<NavigationMenuLink asChild>
<div
style={{ width: containerRef?.current?.offsetWidth - 32 }}
className="flex overflow-x-auto w-[80vw]"
>
<div className="w-[20%] p-4 bg-muted">
<div className="flex gap-1">
<IoIosMore size={22} />
<p className="font-bold">{t("other")}</p>
</div>
</div>
<div className="w-[80%] p-4">
<div className="flex flex-col flex-wrap w-min gap-8 h-[30vh] max-h-[30vh]">
{cateData
.slice(fitCategoriesCount)
.map((subcategory) => (
<div key={subcategory.id}>
<CustomLink
href={buildCategoryUrl(subcategory.slug)}
className="font-semibold whitespace-nowrap text-sm hover:text-primary"
>
{subcategory.translated_name}
</CustomLink>
{subcategory.subcategories_count > 0 && (
<ul className="flex flex-col gap-2 mt-2">
{subcategory?.subcategories
?.slice(0, 5)
.map((nestedSubcategory) => (
<li
key={nestedSubcategory?.id}
className="text-xs"
>
<CustomLink
href={buildCategoryUrl(
nestedSubcategory?.slug
)}
className="hover:text-primary"
>
{nestedSubcategory?.translated_name}
</CustomLink>
</li>
))}
{subcategory.subcategories.length > 5 && (
<li className="text-xs">
<CustomLink
href={buildCategoryUrl(
subcategory.slug
)}
className="hover:text-primary"
>
{t("viewAll")}
</CustomLink>
</li>
)}
</ul>
)}
</div>
))}
</div>
</div>
</div>
</NavigationMenuLink>
</NavigationMenuContent>
</NavigationMenuItem>
)}
</NavigationMenuList>
</NavigationMenu>
</div>
</div>
);
};
export default HeaderCategories;

View File

@@ -0,0 +1,110 @@
"use client";
import { useEffect, useState } from "react";
import AllItems from "./AllItems";
import FeaturedSections from "./FeaturedSections";
import { FeaturedSectionApi, sliderApi } from "@/utils/api";
import { getCurrentLangCode } from "@/redux/reducer/languageSlice";
import { useSelector } from "react-redux";
import { getCityData, getKilometerRange } from "@/redux/reducer/locationSlice";
import OfferSliderSkeleton from "@/components/PagesComponent/Home/OfferSliderSkeleton";
import FeaturedSectionsSkeleton from "./FeaturedSectionsSkeleton";
import PopularCategories from "./PopularCategories";
import dynamic from "next/dynamic";
const OfferSlider = dynamic(() => import("./OfferSlider"), {
ssr: false,
loading: OfferSliderSkeleton,
});
const Home = () => {
const KmRange = useSelector(getKilometerRange);
const cityData = useSelector(getCityData);
const currentLanguageCode = useSelector(getCurrentLangCode);
const [IsFeaturedLoading, setIsFeaturedLoading] = useState(false);
const [featuredData, setFeaturedData] = useState([]);
const [Slider, setSlider] = useState([]);
const [IsSliderLoading, setIsSliderLoading] = useState(true);
const allEmpty = featuredData?.every((ele) => ele?.section_data.length === 0);
useEffect(() => {
const fetchSliderData = async () => {
let params = {};
if (cityData?.city) {
params.city = cityData.city;
params.state = cityData.state;
params.country = cityData.country;
} else if (cityData?.state) {
params.state = cityData.state;
} else if (cityData?.country) {
params.country = cityData.country;
}
try {
const response = await sliderApi.getSlider(params);
const data = response.data;
setSlider(data.data);
} catch (error) {
console.error("Error:", error);
} finally {
setIsSliderLoading(false);
}
};
fetchSliderData();
}, [cityData?.city, cityData?.state, cityData?.country]);
useEffect(() => {
const fetchFeaturedSectionData = async () => {
setIsFeaturedLoading(true);
try {
const params = {};
if (Number(KmRange) > 0 && (cityData?.areaId || cityData?.city)) {
params.radius = KmRange;
params.latitude = cityData.lat;
params.longitude = cityData.long;
} else {
if (cityData?.areaId) {
params.area_id = cityData.areaId;
} else if (cityData?.city) {
params.city = cityData.city;
} else if (cityData?.state) {
params.state = cityData.state;
} else if (cityData?.country) {
params.country = cityData.country;
}
}
const response = await FeaturedSectionApi.getFeaturedSections(params);
const { data } = response.data;
setFeaturedData(data);
} catch (error) {
console.error("Error:", error);
} finally {
setIsFeaturedLoading(false);
}
};
fetchFeaturedSectionData();
}, [cityData.lat, cityData.long, KmRange, currentLanguageCode]);
return (
<>
{IsSliderLoading ? (
<OfferSliderSkeleton />
) : (
Slider &&
Slider.length > 0 && (
<OfferSlider Slider={Slider} IsLoading={IsSliderLoading} />
)
)}
<PopularCategories />
{IsFeaturedLoading ? (
<FeaturedSectionsSkeleton />
) : (
<FeaturedSections
featuredData={featuredData}
setFeaturedData={setFeaturedData}
allEmpty={allEmpty}
/>
)}
<AllItems cityData={cityData} KmRange={KmRange} />
</>
);
};
export default Home;

View File

@@ -0,0 +1,435 @@
"use client";
import LanguageDropdown from "@/components/Common/LanguageDropdown";
import { CurrentLanguageData } from "@/redux/reducer/languageSlice";
import {
getIsFreAdListing,
getOtpServiceProvider,
settingsData,
} from "@/redux/reducer/settingSlice";
import { t, truncate } from "@/utils";
import CustomLink from "@/components/Common/CustomLink";
import { useSelector } from "react-redux";
import { GrLocation } from "react-icons/gr";
import { getCityData } from "@/redux/reducer/locationSlice";
import HomeMobileMenu from "./HomeMobileMenu.jsx";
import MailSentSuccessModal from "@/components/Auth/MailSentSuccessModal.jsx";
import { useEffect, useState } from "react";
import {
getIsLoggedIn,
logoutSuccess,
userSignUpData,
} from "@/redux/reducer/authSlice.js";
import ProfileDropdown from "./ProfileDropdown.jsx";
import { toast } from "sonner";
import FirebaseData from "@/utils/Firebase.js";
import { IoIosAddCircleOutline } from "react-icons/io";
import dynamic from "next/dynamic";
import {
getIsLoginModalOpen,
setIsLoginOpen,
} from "@/redux/reducer/globalStateSlice.js";
import ReusableAlertDialog from "@/components/Common/ReusableAlertDialog";
import { deleteUserApi, getLimitsApi, logoutApi } from "@/utils/api.js";
import { useMediaQuery } from "usehooks-ts";
import UnauthorizedModal from "@/components/Auth/UnauthorizedModal.jsx";
import CustomImage from "@/components/Common/CustomImage.jsx";
import { Loader2 } from "lucide-react";
import { useNavigate } from "@/components/Common/useNavigate.jsx";
import { usePathname } from "next/navigation.js";
import { Skeleton } from "@/components/ui/skeleton.jsx";
import HeaderCategories from "./HeaderCategories.jsx";
import { deleteUser, getAuth } from "firebase/auth";
import DeleteAccountVerifyOtpModal from "@/components/Auth/DeleteAccountVerifyOtpModal.jsx";
import useGetCategories from "@/components/Layout/useGetCategories.jsx";
const Search = dynamic(() => import("./Search.jsx"), {
ssr: false,
});
const LoginModal = dynamic(() => import("@/components/Auth/LoginModal.jsx"), {
ssr: false,
});
const RegisterModal = dynamic(
() => import("@/components/Auth/RegisterModal.jsx"),
{
ssr: false,
}
);
const LocationModal = dynamic(
() => import("@/components/Location/LocationModal.jsx"),
{
ssr: false,
}
);
const HeaderCategoriesSkeleton = () => {
return (
<div className="container">
<div className="py-1.5 border-b">
<Skeleton className="w-full h-[40px]" />
</div>
</div>
);
};
const HomeHeader = () => {
// 📦 Framework & Firebase
const { navigate } = useNavigate();
const { signOut } = FirebaseData();
const pathname = usePathname();
// 🔌 Redux State (via useSelector)
// User & Auth
const userData = useSelector(userSignUpData);
const IsLoggedin = useSelector(getIsLoggedIn);
const IsLoginOpen = useSelector(getIsLoginModalOpen);
const otp_service_provider = useSelector(getOtpServiceProvider);
// Ads & Categories
// const isCategoryLoading = useSelector(getIsCatLoading);
// const cateData = useSelector(CategoryData);
const { getCategories, cateData, isCatLoading: isCategoryLoading } = useGetCategories();
const IsFreeAdListing = useSelector(getIsFreAdListing);
// Location
const cityData = useSelector(getCityData);
// Language & Settings
const CurrentLanguage = useSelector(CurrentLanguageData);
const settings = useSelector(settingsData);
// 🎛️ Local UI State (via useState)
// Modals
const [IsRegisterModalOpen, setIsRegisterModalOpen] = useState(false);
const [IsLocationModalOpen, setIsLocationModalOpen] = useState(false);
const [IsVerifyOtpBeforeDelete, setIsVerifyOtpBeforeDelete] = useState(false);
// Auth State
const [IsLogout, setIsLogout] = useState(false);
const [IsLoggingOut, setIsLoggingOut] = useState(false);
// Profile
const [IsUpdatingProfile, setIsUpdatingProfile] = useState(false);
// Ad Listing
const [IsAdListingClicked, setIsAdListingClicked] = useState(false);
// Email Status
const [IsMailSentSuccess, setIsMailSentSuccess] = useState(false);
// 📱 Media Query
const isLargeScreen = useMediaQuery("(min-width: 992px)");
//delete account state
const [manageDeleteAccount, setManageDeleteAccount] = useState({
IsDeleteAccount: false,
IsDeleting: false,
});
useEffect(() => {
getCategories(1);
}, [CurrentLanguage.id]);
const handleLogout = async () => {
try {
setIsLoggingOut(true);
await signOut();
const res = await logoutApi.logoutApi({
...(userData?.fcm_id && { fcm_token: userData?.fcm_id }),
});
if (res?.data?.error === false) {
logoutSuccess();
toast.success(t("signOutSuccess"));
setIsLogout(false);
// avoid redirect if already on home page otherwise router.push triggering server side api calls
if (pathname !== "/") {
navigate("/");
}
} else {
toast.error(res?.data?.message);
}
} catch (error) {
console.log("Failed to log out", error);
} finally {
setIsLoggingOut(false);
}
};
const handleAdListing = async () => {
if (!IsLoggedin) {
setIsLoginOpen(true);
return;
}
if (!userData?.name || !userData?.email) {
setIsUpdatingProfile(true);
return;
}
if (IsFreeAdListing) {
navigate("/ad-listing");
return;
}
try {
setIsAdListingClicked(true);
const res = await getLimitsApi.getLimits({
package_type: "item_listing",
});
if (res?.data?.error === false) {
navigate("/ad-listing");
} else {
toast.error(t("purchasePlan"));
navigate("/subscription");
}
} catch (error) {
console.log(error);
} finally {
setIsAdListingClicked(false);
}
};
const handleUpdateProfile = () => {
setIsUpdatingProfile(false);
navigate("/profile");
};
const locationText =
cityData?.address_translated || cityData?.formattedAddress;
const handleDeleteAcc = async () => {
try {
setManageDeleteAccount((prev) => ({ ...prev, IsDeleting: true }));
const auth = getAuth();
const user = auth.currentUser;
const isMobileLogin = userData?.type == "phone";
const needsOtpVerification = isMobileLogin && !user && otp_service_provider === "firebase";
if (user) {
await deleteUser(user);
} else if (needsOtpVerification) {
setManageDeleteAccount((prev) => ({ ...prev, IsDeleteAccount: false }));
setIsVerifyOtpBeforeDelete(true);
return;
}
await deleteUserApi.deleteUser();
logoutSuccess();
toast.success(t("userDeleteSuccess"));
setManageDeleteAccount((prev) => ({ ...prev, IsDeleteAccount: false }));
// avoid redirect if already on home page otherwise router.push triggering server side api calls
if (pathname !== "/") {
navigate("/");
}
} catch (error) {
console.error("Error deleting user:", error.message);
const isMobileLogin = userData?.type === "phone";
if (error.code === "auth/requires-recent-login") {
if (isMobileLogin) {
setManageDeleteAccount((prev) => ({
...prev,
IsDeleteAccount: false, // close delete modal
}));
setIsVerifyOtpBeforeDelete(true); // open OTP screen
return;
}
logoutSuccess();
toast.error(t("deletePop"));
setManageDeleteAccount((prev) => ({ ...prev, IsDeleteAccount: false }));
}
} finally {
setManageDeleteAccount((prev) => ({ ...prev, IsDeleting: false }));
}
};
return (
<>
<header className="py-5 border-b">
<nav className="container">
<div className="space-between">
<CustomLink href="/">
<CustomImage
src={settings?.header_logo}
alt="logo"
width={195}
height={52}
className="w-full h-[52px] object-contain ltr:object-left rtl:object-right max-w-[195px]"
/>
</CustomLink>
{/* desktop category search select */}
{isLargeScreen && (
<div className="flex items-center border leading-none rounded">
<Search />
</div>
)}
<button
className="hidden lg:flex items-center gap-1"
onClick={() => setIsLocationModalOpen(true)}
>
<GrLocation
size={16}
className="flex-shrink-0"
title={locationText ? locationText : t("addLocation")}
/>
<p
className="hidden xl:block text-sm"
title={locationText ? locationText : t("addLocation")}
>
{locationText
? truncate(locationText, 12)
: truncate(t("addLocation"), 12)}
</p>
</button>
<div className="hidden lg:flex items-center gap-2">
{IsLoggedin ? (
<ProfileDropdown
setIsLogout={setIsLogout}
IsLogout={IsLogout}
/>
) : (
<>
<button
onClick={() => setIsLoginOpen(true)}
title={t("login")}
>
{truncate(t("login"), 12)}
</button>
<span className="border-l h-6 self-center"></span>
<button
onClick={() => setIsRegisterModalOpen(true)}
title={t("register")}
>
{truncate(t("register"), 12)}
</button>
</>
)}
<button
className="bg-primary px-2 xl:px-4 py-2 items-center text-white rounded-md flex gap-1"
disabled={IsAdListingClicked}
onClick={handleAdListing}
title={t("adListing")}
>
{IsAdListingClicked ? (
<Loader2 size={18} className="animate-spin" />
) : (
<IoIosAddCircleOutline size={18} />
)}
<span className="hidden xl:inline">
{truncate(t("adListing"), 12)}
</span>
</button>
<LanguageDropdown />
</div>
<HomeMobileMenu
setIsLocationModalOpen={setIsLocationModalOpen}
setIsRegisterModalOpen={setIsRegisterModalOpen}
setIsLogout={setIsLogout}
locationText={locationText}
handleAdListing={handleAdListing}
IsAdListingClicked={IsAdListingClicked}
setManageDeleteAccount={setManageDeleteAccount}
/>
</div>
{!isLargeScreen && (
<div className="flex items-center border leading-none rounded mt-2">
<Search />
</div>
)}
</nav>
</header>
{isCategoryLoading && !cateData.length ? (
<HeaderCategoriesSkeleton />
) : (
cateData &&
cateData.length > 0 && <HeaderCategories cateData={cateData} />
)}
<LoginModal
key={IsLoginOpen}
IsLoginOpen={IsLoginOpen}
setIsRegisterModalOpen={setIsRegisterModalOpen}
/>
<RegisterModal
setIsMailSentSuccess={setIsMailSentSuccess}
IsRegisterModalOpen={IsRegisterModalOpen}
setIsRegisterModalOpen={setIsRegisterModalOpen}
key={`${IsRegisterModalOpen}-register-modal`}
/>
<MailSentSuccessModal
IsMailSentSuccess={IsMailSentSuccess}
setIsMailSentSuccess={setIsMailSentSuccess}
/>
{/* Reusable Alert Dialog for Logout */}
<ReusableAlertDialog
open={IsLogout}
onCancel={() => setIsLogout(false)}
onConfirm={handleLogout}
title={t("confirmLogout")}
description={t("areYouSureToLogout")}
cancelText={t("cancel")}
confirmText={t("yes")}
confirmDisabled={IsLoggingOut}
/>
{/* Reusable Alert Dialog for Updating Profile */}
<ReusableAlertDialog
open={IsUpdatingProfile}
onCancel={() => setIsUpdatingProfile(false)}
onConfirm={handleUpdateProfile}
title={t("updateProfile")}
description={t("youNeedToUpdateProfile")}
confirmText={t("yes")}
/>
{!isLargeScreen && (
<ReusableAlertDialog
open={manageDeleteAccount?.IsDeleteAccount}
onCancel={() =>
setManageDeleteAccount((prev) => ({
...prev,
IsDeleteAccount: false,
}))
}
onConfirm={handleDeleteAcc}
title={t("areYouSure")}
description={
<ul className="list-disc list-inside mt-2">
<li>{t("adsAndTransactionWillBeDeleted")}</li>
<li>{t("accountsDetailsWillNotRecovered")}</li>
<li>{t("subWillBeCancelled")}</li>
<li>{t("savedMesgWillBeLost")}</li>
</ul>
}
cancelText={t("cancel")}
confirmText={t("yes")}
confirmDisabled={manageDeleteAccount?.IsDeleting}
/>
)}
<LocationModal
key={`${IsLocationModalOpen}-location-modal`}
IsLocationModalOpen={IsLocationModalOpen}
setIsLocationModalOpen={setIsLocationModalOpen}
/>
<UnauthorizedModal />
<DeleteAccountVerifyOtpModal
isOpen={IsVerifyOtpBeforeDelete}
setIsOpen={setIsVerifyOtpBeforeDelete}
key={`${IsVerifyOtpBeforeDelete}-delete-account-verify-otp-modal`}
pathname={pathname}
navigate={navigate}
/>
</>
);
};
export default HomeHeader;

View File

@@ -0,0 +1,260 @@
"use client";
import {
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
SheetTrigger,
} from "@/components/ui/sheet";
import { t } from "@/utils";
import { useState } from "react";
import { GiHamburgerMenu } from "react-icons/gi";
import LanguageDropdown from "../../Common/LanguageDropdown";
import { GrLocation } from "react-icons/gr";
import {
IoIosAddCircleOutline,
IoMdNotificationsOutline,
} from "react-icons/io";
import { setIsLoginOpen } from "@/redux/reducer/globalStateSlice";
import { usePathname } from "next/navigation";
import CustomImage from "@/components/Common/CustomImage";
import { Loader2 } from "lucide-react";
import { useSelector } from "react-redux";
import { userSignUpData } from "@/redux/reducer/authSlice";
import CustomLink from "@/components/Common/CustomLink";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { BiChat, BiDollarCircle, BiReceipt, BiTrashAlt } from "react-icons/bi";
import { LiaAdSolid } from "react-icons/lia";
import { LuHeart } from "react-icons/lu";
import { MdOutlineRateReview, MdWorkOutline } from "react-icons/md";
import { RiLogoutCircleLine } from "react-icons/ri";
import { settingsData } from "@/redux/reducer/settingSlice";
import FilterTree from "@/components/Filter/FilterTree";
const HomeMobileMenu = ({
setIsLocationModalOpen,
setIsRegisterModalOpen,
setIsLogout,
locationText,
handleAdListing,
IsAdListingClicked,
setManageDeleteAccount,
}) => {
const UserData = useSelector(userSignUpData);
const settings = useSelector(settingsData);
const [isOpen, setIsOpen] = useState(false);
const pathname = usePathname();
const showMenu = !!UserData;
const showCategories = !pathname.startsWith("/ads");
const openLocationEditModal = () => {
setIsOpen(false);
setIsLocationModalOpen(true);
};
const handleLogin = () => {
setIsOpen(false);
setIsLoginOpen(true);
};
const handleRegister = () => {
setIsOpen(false);
setIsRegisterModalOpen(true);
};
const handleSignOut = () => {
setIsOpen(false);
setIsLogout(true);
};
const handleDeleteAccount = () => {
setIsOpen(false);
setManageDeleteAccount((prev) => ({
...prev,
IsDeleteAccount: true,
}));
};
// All user links
const navItems = (
<div className="flex flex-col px-4 pb-4">
<CustomLink
href="/notifications"
className="flex items-center gap-1 py-4"
>
<IoMdNotificationsOutline size={24} />
<span>{t("notifications")}</span>
</CustomLink>
<CustomLink href="/chat" className="flex items-center gap-1 py-4">
<BiChat size={24} />
<span>{t("chat")}</span>
</CustomLink>
<CustomLink
href="/user-subscription"
className="flex items-center gap-1 py-4"
>
<BiDollarCircle size={24} />
<span>{t("subscription")}</span>
</CustomLink>
<CustomLink href="/my-ads" className="flex items-center gap-1 py-4">
<LiaAdSolid size={24} />
<span>{t("myAds")}</span>
</CustomLink>
<CustomLink href="/favorites" className="flex items-center gap-1 py-4">
<LuHeart size={24} />
<span>{t("favorites")}</span>
</CustomLink>
<CustomLink href="/transactions" className="flex items-center gap-1 py-4">
<BiReceipt size={24} />
<span>{t("transaction")}</span>
</CustomLink>
<CustomLink href="/reviews" className="flex items-center gap-1 py-4">
<MdOutlineRateReview size={24} />
<span>{t("myReviews")}</span>
</CustomLink>
<CustomLink
href="/job-applications"
className="flex items-center gap-1 py-4"
>
<MdWorkOutline size={24} />
<span>{t("jobApplications")}</span>
</CustomLink>
<button onClick={handleSignOut} className="flex items-center gap-1 py-4">
<RiLogoutCircleLine size={24} />
<span>{t("signOut")}</span>
</button>
<button
onClick={handleDeleteAccount}
className="flex items-center gap-1 text-destructive py-4"
>
<BiTrashAlt size={24} />
<span>{t("deleteAccount")}</span>
</button>
</div>
);
return (
<Sheet open={isOpen} onOpenChange={setIsOpen} className="lg:hidden">
<SheetTrigger asChild className="lg:hidden">
<button
id="hamburg"
className="text-2xl cursor-pointer border rounded-lg p-1"
>
<GiHamburgerMenu size={25} className="text-primary" />
</button>
</SheetTrigger>
<SheetContent className="[&>button:first-child]:hidden] p-0 overflow-y-auto">
<SheetHeader className="p-4 border-b border">
<SheetTitle>
<CustomImage
src={settings?.header_logo}
width={195}
height={92}
alt="Logo"
className="w-full h-[52px] object-contain ltr:object-left rtl:object-right max-w-[195px]"
/>
</SheetTitle>
<SheetDescription className="sr-only"></SheetDescription>
</SheetHeader>
<div className="p-4 flex flex-col gap-4">
<div className="flex items-center justify-between gap-3">
{UserData ? (
<CustomLink href="/profile" className="flex items-center gap-2">
<CustomImage
src={UserData?.profile}
width={48}
height={48}
alt={UserData?.name}
className="rounded-full size-12 aspect-square object-cover border"
onClick={() => setIsOpen(false)}
/>
<p
className="line-clamp-2"
title={UserData?.name}
onClick={() => setIsOpen(false)}
>
{UserData?.name}
</p>
</CustomLink>
) : (
<div className="flex items-center gap-2">
<button onClick={handleLogin}>{t("login")}</button>
<span className="border-l h-6 self-center"></span>
<button onClick={handleRegister}>{t("register")}</button>
</div>
)}
<div className="flex-shrink-0">
<LanguageDropdown />
</div>
</div>
<div
className="flex items-center gap-1 cursor-pointer"
onClick={openLocationEditModal}
>
<GrLocation size={16} className="flex-shrink-0" />
<p
className="line-clamp-2"
title={locationText ? locationText : t("addLocation")}
>
{locationText ? locationText : t("addLocation")}
</p>
</div>
<button
className="flex items-center justify-center gap-2 bg-primary py-2 px-3 text-white rounded-md"
disabled={IsAdListingClicked}
onClick={handleAdListing}
>
{IsAdListingClicked ? (
<Loader2 size={18} className="animate-spin" />
) : (
<IoIosAddCircleOutline size={18} />
)}
<span>{t("adListing")}</span>
</button>
</div>
{showMenu && showCategories ? (
<Tabs defaultValue="menu">
<TabsList className="flex items-center justify-between bg-muted rounded-none">
<TabsTrigger
value="menu"
className="flex-1 data-state-active:bg-primary"
>
{t("menu")}
</TabsTrigger>
<TabsTrigger
value="categories"
className="flex-1 data-state-active:bg-primary"
>
{t("multipleCategories")}
</TabsTrigger>
</TabsList>
<TabsContent value="menu">
{navItems}
</TabsContent>
<TabsContent value="categories" className="mt-4 px-4 pb-4">
<FilterTree />
</TabsContent>
</Tabs>
) : showMenu ? (
navItems
) : showCategories ? (
<div className="px-4 pb-4 flex flex-col gap-4">
<h1 className="font-medium">{t("multipleCategories")}</h1>
<FilterTree />
</div>
) : null}
</SheetContent>
</Sheet>
);
};
export default HomeMobileMenu;

View File

@@ -0,0 +1,124 @@
"use client";
import {
Carousel,
CarouselContent,
CarouselItem,
} from "@/components/ui/carousel";
import { useEffect, useState } from "react";
import { RiArrowLeftLine, RiArrowRightLine } from "react-icons/ri";
import Autoplay from "embla-carousel-autoplay";
import { userSignUpData } from "@/redux/reducer/authSlice";
import { useSelector } from "react-redux";
import CustomLink from "@/components/Common/CustomLink";
import { getIsRtl } from "@/redux/reducer/languageSlice";
import CustomImage from "@/components/Common/CustomImage";
const OfferSlider = ({ Slider }) => {
const [api, setApi] = useState();
const [current, setCurrent] = useState(0);
const userData = useSelector(userSignUpData);
const isRTL = useSelector(getIsRtl);
useEffect(() => {
if (!api) {
return;
}
setCurrent(api.selectedScrollSnap());
api.on("select", () => {
setCurrent(api.selectedScrollSnap());
});
}, [api]);
return (
<section className="py-6 bg-muted">
<div className="container">
<Carousel
key={isRTL ? "rtl" : "ltr"}
className="w-full"
setApi={setApi}
opts={{
align: "start",
containScroll: "trim",
direction: isRTL ? "rtl" : "ltr",
}}
plugins={[Autoplay({ delay: 3000 })]}
>
<CarouselContent className="-ml-3 md:-ml-[30px]">
{Slider.map((ele, index) => {
let href;
if (ele?.model_type === "App\\Models\\Item") {
if (userData && userData?.id === ele?.model?.user_id) {
href = `/my-listing/${ele?.model?.slug}`;
} else {
href = `/ad-details/${ele?.model?.slug}`;
}
} else if (ele?.model_type === null) {
href = ele?.third_party_link;
} else if (ele?.model_type === "App\\Models\\Category") {
href = `/ads?category=${ele.model.slug}`;
} else {
href = "/";
}
// First 2 images load with eager loading and priority for Lighthouse performance
const isPriorityImage = index < 2;
return (
<CarouselItem
className="basis-full md:basis-2/3 pl-3 md:pl-[30px]"
key={ele?.id}
>
<CustomLink
href={href}
target={ele?.model_type === null ? "_blank" : ""}
>
<CustomImage
src={ele.image}
alt="slider imag"
width={983}
height={493}
className="aspect-[983/493] w-full object-cover rounded-xl"
loading={isPriorityImage ? "eager" : "lazy"}
priority={isPriorityImage}
/>
</CustomLink>
</CarouselItem>
);
})}
</CarouselContent>
{Slider && Slider?.length > 1 && (
<>
<button
onClick={() => api?.scrollTo(current - 1)}
className={`sm:block absolute z-10 top-1/2 -translate-y-1/2 ltr:left-2 rtl:right-2 bg-primary p-1 md:p-2 rounded-full ${
!api?.canScrollPrev() ? "cursor-default" : ""
}`}
disabled={!api?.canScrollPrev()}
>
<RiArrowLeftLine
size={24}
color="white"
className={isRTL ? "rotate-180" : ""}
/>
</button>
<button
onClick={() => api?.scrollTo(current + 1)}
className={`sm:block absolute z-10 top-1/2 -translate-y-1/2 ltr:right-2 rtl:left-2 bg-primary p-1 md:p-2 rounded-full ${
!api?.canScrollNext() ? "cursor-default" : ""
}`}
disabled={!api?.canScrollNext()}
>
<RiArrowRightLine
size={24}
color="white"
className={isRTL ? "rotate-180" : ""}
/>
</button>
</>
)}
</Carousel>
</div>
</section>
);
};
export default OfferSlider;

View File

@@ -0,0 +1,23 @@
import { Skeleton } from "@/components/ui/skeleton";
const OfferSliderSkeleton = () => {
return (
<section className="py-6 bg-muted">
<div className="container overflow-hidden">
<div className="grid grid-cols-1 md:grid-cols-[66.66%_66.66%] gap-4">
{Array.from({ length: 2 }).map((_, index) => (
<Skeleton
className={`${
index === 1 ? "hidden md:block" : ""
} aspect-[983/493] w-full rounded-xl`}
key={index}
height={493}
/>
))}
</div>
</div>
</section>
);
};
export default OfferSliderSkeleton;

View File

@@ -0,0 +1,123 @@
"use client";
import { useEffect, useState } from "react";
import { RiArrowLeftLine, RiArrowRightLine } from "react-icons/ri";
import {
Carousel,
CarouselContent,
CarouselItem,
} from "@/components/ui/carousel";
import PopularCategoriesSkeleton from "./PopularCategoriesSkeleton.jsx";
import PopularCategoryCard from "@/components/PagesComponent/Home/PopularCategoryCard";
import { useSelector } from "react-redux";
import { t } from "@/utils";
import { getIsRtl } from "@/redux/reducer/languageSlice.js";
import { Loader2 } from "lucide-react";
import useGetCategories from "@/components/Layout/useGetCategories.jsx";
const PopularCategories = () => {
const {
cateData,
getCategories,
isCatLoading,
isCatLoadMore,
catLastPage,
catCurrentPage,
} = useGetCategories();
const isRTL = useSelector(getIsRtl);
const [api, setApi] = useState();
const [current, setCurrent] = useState(0);
const isNextDisabled =
isCatLoadMore ||
((!api || !api.canScrollNext()) && catCurrentPage >= catLastPage);
useEffect(() => {
if (!api) {
return;
}
setCurrent(api.selectedScrollSnap());
api.on("select", () => {
setCurrent(api.selectedScrollSnap());
});
}, [api, cateData.length]);
const handleNext = async () => {
if (api && api.canScrollNext()) {
api.scrollTo(current + 1);
} else if (catCurrentPage < catLastPage) {
await getCategories(catCurrentPage + 1);
setTimeout(() => {
api.scrollTo(current + 1);
}, 200);
}
};
return isCatLoading && !cateData.length ? (
<PopularCategoriesSkeleton />
) : (
cateData && cateData.length > 0 && (
<section className="container mt-12">
<div className="space-between">
<h5 className="text-xl sm:text-2xl font-medium">
{t("popularCategories")}
</h5>
<div className="flex items-center justify-center gap-2 sm:gap-4">
<button
onClick={() => api && api.scrollTo(current - 1)}
className={`bg-primary p-1 sm:p-2 rounded-full ${
!api?.canScrollPrev() ? "opacity-65 cursor-default" : ""
}`}
disabled={!api?.canScrollPrev()}
>
<RiArrowLeftLine
size={24}
color="white"
className={isRTL ? "rotate-180" : ""}
/>
</button>
<button
onClick={handleNext}
className={`bg-primary p-1 sm:p-2 rounded-full ${
isNextDisabled ? "opacity-65 cursor-default" : ""
}`}
disabled={isNextDisabled}
>
{isCatLoadMore ? (
<Loader2 size={24} className="animate-spin" />
) : (
<RiArrowRightLine
size={24}
color="white"
className={isRTL ? "rotate-180" : ""}
/>
)}
</button>
</div>
</div>
<Carousel
key={isRTL ? "rtl" : "ltr"}
className="w-full mt-6"
setApi={setApi}
opts={{
align: "start",
containScroll: "trim",
direction: isRTL ? "rtl" : "ltr",
}}
>
<CarouselContent className="-ml-3 md:-ml-[30px]">
{cateData.map((item) => (
<CarouselItem
key={item?.id}
className="basis-1/3 sm:basis-1/4 md:basis-1/5 lg:basis-[16.66%] xl:basis-[12.5%] 2xl:basis-[11.11%] md:pl-[30px]"
>
<PopularCategoryCard item={item} />
</CarouselItem>
))}
</CarouselContent>
</Carousel>
</section>
)
);
};
export default PopularCategories;

View File

@@ -0,0 +1,38 @@
import { Carousel, CarouselContent, CarouselItem } from "../../ui/carousel";
import { Skeleton } from "../../ui/skeleton";
const PopularCategoriesSkeleton = () => {
return (
<>
<div className="container mt-12">
<div className="space-between">
<Skeleton className="w-1/4 h-4" />
<Skeleton className="w-1/12 h-4" />
</div>
<Carousel
className="w-full mt-6"
opts={{
align: "start",
containScroll: "trim",
}}
>
<CarouselContent className="-ml-3 md:-ml-[30px]">
{Array.from({ length: 9 }).map((_, index) => (
<CarouselItem
key={index}
className="basis-1/3 sm:basis-1/4 md:basis-1/5 lg:basis-[16.66%] xl:basis-[12.5%] 2xl:basis-[11.11%] md:pl-[30px]"
>
<div className="flex flex-col gap-4">
<Skeleton className="w-full aspect-square rounded-full" />
<Skeleton className="w-full h-4" />
</div>
</CarouselItem>
))}
</CarouselContent>
</Carousel>
</div>
</>
);
};
export default PopularCategoriesSkeleton;

View File

@@ -0,0 +1,28 @@
import CustomLink from "@/components/Common/CustomLink";
import CustomImage from "@/components/Common/CustomImage";
const PopularCategoryCard = ({ item }) => {
return (
<CustomLink
href={`/ads?category=${item?.slug}`}
className="flex flex-col gap-4"
>
<div className="border p-2.5 rounded-full">
<CustomImage
src={item?.image}
width={96}
height={96}
className="aspect-square w-full rounded-full"
alt="Category"
loading="eager"
/>
</div>
<p className="text-sm sm:text-base line-clamp-2 font-medium text-center leading-tight">
{item?.translated_name}
</p>
</CustomLink>
);
};
export default PopularCategoryCard;

View File

@@ -0,0 +1,121 @@
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { userSignUpData } from "@/redux/reducer/authSlice";
import { t, truncate } from "@/utils";
import { useSelector } from "react-redux";
import { FiUser } from "react-icons/fi";
import { IoMdNotificationsOutline } from "react-icons/io";
import { BiChat, BiDollarCircle, BiReceipt } from "react-icons/bi";
import { LiaAdSolid } from "react-icons/lia";
import { LuHeart } from "react-icons/lu";
import { MdOutlineRateReview, MdWorkOutline } from "react-icons/md";
import { RiLogoutCircleLine } from "react-icons/ri";
import { FaAngleDown } from "react-icons/fa";
import { useMediaQuery } from "usehooks-ts";
import CustomImage from "@/components/Common/CustomImage";
import { useNavigate } from "@/components/Common/useNavigate";
const ProfileDropdown = ({ IsLogout, setIsLogout }) => {
const isSmallScreen = useMediaQuery("(max-width: 1200px)");
const { navigate } = useNavigate();
const UserData = useSelector(userSignUpData);
return (
<DropdownMenu key={IsLogout}>
<DropdownMenuTrigger className="flex items-center gap-1">
<CustomImage
src={UserData?.profile}
alt={UserData?.name}
width={32}
height={32}
className="rounded-full w-8 h-8 aspect-square object-cover border"
/>
<p>{truncate(UserData.name, 12)}</p>
<FaAngleDown className="text-muted-foreground flex-shrink-0" size={12} />
</DropdownMenuTrigger>
<DropdownMenuContent align={isSmallScreen ? "start" : "center"}>
<DropdownMenuItem
className="cursor-pointer"
onClick={() => navigate("/profile")}
>
<FiUser size={16} />
<span>{t("myProfile")}</span>
</DropdownMenuItem>
<DropdownMenuItem
className="cursor-pointer"
onClick={() => navigate("/notifications")}
>
<IoMdNotificationsOutline size={16} />
{t("notification")}
</DropdownMenuItem>
<DropdownMenuItem
className="cursor-pointer"
onClick={() => navigate("/chat")}
>
<BiChat size={16} />
{t("chat")}
</DropdownMenuItem>
<DropdownMenuItem
className="cursor-pointer"
onClick={() => navigate("/user-subscription")}
>
<BiDollarCircle size={16} />
{t("subscription")}
</DropdownMenuItem>
<DropdownMenuItem
className="cursor-pointer"
onClick={() => navigate("/my-ads")}
>
<LiaAdSolid size={16} />
{t("myAds")}
</DropdownMenuItem>
<DropdownMenuItem
className="cursor-pointer"
onClick={() => navigate("/favorites")}
>
<LuHeart size={16} />
{t("favorites")}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => navigate("/transactions")}>
<BiReceipt size={16} />
{t("transaction")}
</DropdownMenuItem>
<DropdownMenuItem
className="cursor-pointer"
onClick={() => navigate("/reviews")}
>
<MdOutlineRateReview size={16} />
{t("myReviews")}
</DropdownMenuItem>
<DropdownMenuItem
className="cursor-pointer"
onClick={() => navigate("/job-applications")}
>
<MdWorkOutline size={16} />
{t("jobApplications")}
</DropdownMenuItem>
<DropdownMenuItem
className="cursor-pointer"
onClick={() => setIsLogout(true)}
>
<RiLogoutCircleLine size={16} />
{t("signOut")}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
};
export default ProfileDropdown;

View File

@@ -0,0 +1,156 @@
"use client";
import { Check, ChevronsUpDown, Loader2 } from "lucide-react";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@/components/ui/command";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { useEffect, useState } from "react";
import { useInView } from "react-intersection-observer";
import { t } from "@/utils";
import { BiPlanet } from "react-icons/bi";
import { FaSearch } from "react-icons/fa";
import { usePathname } from "next/navigation";
import { useNavigate } from "@/components/Common/useNavigate";
import useGetCategories from "@/components/Layout/useGetCategories";
const Search = () => {
const {
cateData,
getCategories,
isCatLoadMore,
catLastPage,
catCurrentPage,
} = useGetCategories();
const pathname = usePathname();
const { navigate } = useNavigate();
const categoryList = [
{ slug: "all-categories", translated_name: t("allCategories") },
...cateData,
];
const [open, setOpen] = useState(false);
const [value, setValue] = useState("all-categories");
const selectedItem = categoryList.find((item) => item.slug === value);
const hasMore = catCurrentPage < catLastPage;
const { ref, inView } = useInView();
const [searchQuery, setSearchQuery] = useState("");
useEffect(() => {
if (open && inView && hasMore && !isCatLoadMore) {
getCategories(catCurrentPage + 1);
}
}, [hasMore, inView, isCatLoadMore, open]);
const handleSearchNav = (e) => {
e.preventDefault();
const query = encodeURIComponent(searchQuery);
// Build the base URL with query and language
const baseUrl = `/ads?query=${query}`;
// Add category parameter if not "all-categories"
const url =
selectedItem?.slug === "all-categories"
? baseUrl
: `/ads?category=${selectedItem?.slug}&query=${query}`;
// Use consistent navigation method
if (pathname === "/ads") {
// If already on ads page, use history API to avoid full page reload
window.history.pushState(null, "", url);
} else {
// If on different page, use router for navigation
navigate(url);
}
};
return (
<>
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={open}
className="min-w-[125px] max-w-[125px] sm:min-w-[156px] sm:max-w-[156px] py-1 px-1.5 sm:py-2 sm:px-3 justify-between border-none hover:bg-transparent font-normal"
>
<span className="truncate">
{selectedItem?.translated_name || t("selectCat")}
</span>
<ChevronsUpDown className="opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent align="start" className="w-[200px] p-0">
<Command>
<CommandInput placeholder={t("searchACategory")} />
<CommandList>
<CommandEmpty>{t("noCategoryFound")}</CommandEmpty>
<CommandGroup>
{categoryList.map((category, index) => {
const isLast = open && index === categoryList.length - 1;
return (
<CommandItem
key={category?.slug}
value={category?.slug}
onSelect={(currentValue) => {
setValue(currentValue);
setOpen(false);
}}
ref={isLast ? ref : null}
>
{category.translated_name || category?.name}
<Check
className={cn(
"ml-auto",
value === category.slug ? "opacity-100" : "opacity-0"
)}
/>
</CommandItem>
);
})}
</CommandGroup>
{isCatLoadMore && (
<div className="flex justify-center items-center pb-2 text-muted-foreground">
<Loader2 className="animate-spin" />
</div>
)}
</CommandList>
</Command>
</PopoverContent>
</Popover>
<form
onSubmit={handleSearchNav}
className="w-full flex items-center gap-2 ltr:border-l rtl:border-r py-1 px-1.5 sm:py-2 sm:px-3"
>
<BiPlanet color="#595B6C" className="min-w-4 min-h-4" />
<input
type="text"
placeholder={t("searchAd")}
className="text-sm outline-none w-full"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
<button
className="flex items-center gap-2 bg-primary text-white p-2 rounded"
type="submit"
>
<FaSearch size={14} />
</button>
</form>
</>
);
};
export default Search;