classify web
This commit is contained in:
165
components/PagesComponent/Home/AllItems.jsx
Normal file
165
components/PagesComponent/Home/AllItems.jsx
Normal 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;
|
||||
14
components/PagesComponent/Home/AllItemsSkeleton.jsx
Normal file
14
components/PagesComponent/Home/AllItemsSkeleton.jsx
Normal 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;
|
||||
62
components/PagesComponent/Home/FeaturedSections.jsx
Normal file
62
components/PagesComponent/Home/FeaturedSections.jsx
Normal 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;
|
||||
20
components/PagesComponent/Home/FeaturedSectionsSkeleton.jsx
Normal file
20
components/PagesComponent/Home/FeaturedSectionsSkeleton.jsx
Normal 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;
|
||||
300
components/PagesComponent/Home/HeaderCategories.jsx
Normal file
300
components/PagesComponent/Home/HeaderCategories.jsx
Normal 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;
|
||||
110
components/PagesComponent/Home/Home.jsx
Normal file
110
components/PagesComponent/Home/Home.jsx
Normal 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;
|
||||
435
components/PagesComponent/Home/HomeHeader.jsx
Normal file
435
components/PagesComponent/Home/HomeHeader.jsx
Normal 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;
|
||||
260
components/PagesComponent/Home/HomeMobileMenu.jsx
Normal file
260
components/PagesComponent/Home/HomeMobileMenu.jsx
Normal 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;
|
||||
124
components/PagesComponent/Home/OfferSlider.jsx
Normal file
124
components/PagesComponent/Home/OfferSlider.jsx
Normal 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;
|
||||
23
components/PagesComponent/Home/OfferSliderSkeleton.jsx
Normal file
23
components/PagesComponent/Home/OfferSliderSkeleton.jsx
Normal 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;
|
||||
123
components/PagesComponent/Home/PopularCategories.jsx
Normal file
123
components/PagesComponent/Home/PopularCategories.jsx
Normal 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;
|
||||
38
components/PagesComponent/Home/PopularCategoriesSkeleton.jsx
Normal file
38
components/PagesComponent/Home/PopularCategoriesSkeleton.jsx
Normal 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;
|
||||
28
components/PagesComponent/Home/PopularCategoryCard.jsx
Normal file
28
components/PagesComponent/Home/PopularCategoryCard.jsx
Normal 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;
|
||||
121
components/PagesComponent/Home/ProfileDropdown.jsx
Normal file
121
components/PagesComponent/Home/ProfileDropdown.jsx
Normal 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;
|
||||
156
components/PagesComponent/Home/Search.jsx
Normal file
156
components/PagesComponent/Home/Search.jsx
Normal 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;
|
||||
Reference in New Issue
Block a user