classify web
This commit is contained in:
173
components/PagesComponent/MyAds/ChoosePackageModal.jsx
Normal file
173
components/PagesComponent/MyAds/ChoosePackageModal.jsx
Normal file
@@ -0,0 +1,173 @@
|
||||
import CustomImage from "@/components/Common/CustomImage";
|
||||
import {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { formatPriceAbbreviated, t } from "@/utils";
|
||||
import { getPackageApi } from "@/utils/api";
|
||||
import { useSelector } from "react-redux";
|
||||
import { getCurrentLangCode } from "@/redux/reducer/languageSlice";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import NoData from "@/components/EmptyStates/NoData";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const ChoosePackageModal = ({
|
||||
IsChoosePackage,
|
||||
setIsChoosePackage,
|
||||
selectedPackageId,
|
||||
setSelectedPackageId,
|
||||
ItemPackages,
|
||||
setItemPackages,
|
||||
isRenewingAd,
|
||||
handleRenew,
|
||||
}) => {
|
||||
const [IsLoading, setIsLoading] = useState(false);
|
||||
const currentLanguageCode = useSelector(getCurrentLangCode);
|
||||
|
||||
useEffect(() => {
|
||||
getItemsPackageData();
|
||||
}, [currentLanguageCode]);
|
||||
|
||||
const getItemsPackageData = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const res = await getPackageApi.getPackage({ type: "item_listing" });
|
||||
const { data } = res?.data;
|
||||
setItemPackages(data || []);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={IsChoosePackage} onOpenChange={setIsChoosePackage}>
|
||||
<DialogContent onInteractOutside={(e) => e.preventDefault()}>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-xl font-semibold">
|
||||
{t("selectPackage")}
|
||||
</DialogTitle>
|
||||
<DialogDescription className="sr-only"></DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="flex flex-col gap-3">
|
||||
{IsLoading ? (
|
||||
Array.from({ length: 3 }).map((_, index) => (
|
||||
<PackageCardSkeleton key={index} />
|
||||
))
|
||||
) : ItemPackages.length > 0 ? (
|
||||
ItemPackages.map((item) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center gap-3 p-3 border rounded-lg hover:bg-muted transition-colors cursor-pointer",
|
||||
item?.id == selectedPackageId &&
|
||||
"bg-primary text-white hover:bg-primary"
|
||||
)}
|
||||
key={item?.id}
|
||||
onClick={() => setSelectedPackageId(item?.id)}
|
||||
>
|
||||
<CustomImage
|
||||
src={item.icon}
|
||||
width={58}
|
||||
height={58}
|
||||
alt={"Dummy image"}
|
||||
className="rounded"
|
||||
/>
|
||||
<div className="flex flex-col gap-1 w-full">
|
||||
<h3 className="text-lg font-medium ltr:text-left rtl:text-right line-clamp-2">
|
||||
{item.translated_name}
|
||||
{item?.is_active && t("activePlan")}
|
||||
</h3>
|
||||
<div className="flex items-center gap-3 justify-between w-full">
|
||||
<span className="text-lg font-bold whitespace-nowrap">
|
||||
{formatPriceAbbreviated(item.final_price)}
|
||||
</span>
|
||||
<span
|
||||
className={cn(
|
||||
"text-muted-foreground text-xs sm:text-sm ltr:text-right rtl:text-left",
|
||||
item?.id == selectedPackageId && "text-white"
|
||||
)}
|
||||
>
|
||||
<strong
|
||||
className={cn(
|
||||
item?.id == selectedPackageId
|
||||
? "text-white"
|
||||
: "text-foreground"
|
||||
)}
|
||||
>
|
||||
{item.item_limit === "unlimited"
|
||||
? t("unlimited")
|
||||
: item.item_limit}
|
||||
</strong>{" "}
|
||||
{t("ads")} |
|
||||
<strong
|
||||
className={cn(
|
||||
item?.id == selectedPackageId
|
||||
? "text-white"
|
||||
: "text-foreground"
|
||||
)}
|
||||
>
|
||||
{item.duration === "unlimited"
|
||||
? t("unlimited")
|
||||
: item.duration}
|
||||
</strong>{" "}
|
||||
{t("days")}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<NoData name="packages" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{ItemPackages.length > 0 && (
|
||||
<DialogFooter>
|
||||
<DialogClose asChild>
|
||||
<Button className="bg-black text-white">{t("cancel")}</Button>
|
||||
</DialogClose>
|
||||
<Button
|
||||
className="bg-primary text-white"
|
||||
type="button"
|
||||
onClick={handleRenew}
|
||||
disabled={isRenewingAd}
|
||||
>
|
||||
{isRenewingAd ? t("loading") : t("renewAd")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChoosePackageModal;
|
||||
|
||||
const PackageCardSkeleton = () => {
|
||||
return (
|
||||
<div className="flex items-center gap-3 p-3 border rounded-lg">
|
||||
{/* Image Skeleton */}
|
||||
<Skeleton className="h-[58px] w-[58px] rounded" />
|
||||
|
||||
<div className="flex flex-col gap-2 w-full">
|
||||
{/* Title Skeleton */}
|
||||
<Skeleton className="h-5 w-3/4" />
|
||||
|
||||
{/* Price + Meta Skeleton */}
|
||||
<div className="flex items-center justify-between w-full gap-3">
|
||||
<Skeleton className="h-5 w-16" />
|
||||
<Skeleton className="h-4 w-16" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
75
components/PagesComponent/MyAds/GetMyAdStatus.jsx
Normal file
75
components/PagesComponent/MyAds/GetMyAdStatus.jsx
Normal file
@@ -0,0 +1,75 @@
|
||||
// 📌 components/Common/GetMyAdStatus.jsx
|
||||
import {
|
||||
MdAirplanemodeInactive,
|
||||
MdOutlineDone,
|
||||
MdOutlineLiveTv,
|
||||
MdOutlineSell,
|
||||
} from "react-icons/md";
|
||||
import { BiBadgeCheck } from "react-icons/bi";
|
||||
import { RxCross2 } from "react-icons/rx";
|
||||
import { RiPassExpiredLine } from "react-icons/ri";
|
||||
import { IoTimerOutline } from "react-icons/io5";
|
||||
import { t } from "@/utils";
|
||||
|
||||
const GetMyAdStatus = ({
|
||||
status,
|
||||
isApprovedSort = false,
|
||||
isFeature = false,
|
||||
isJobCategory = false,
|
||||
}) => {
|
||||
const statusComponents = {
|
||||
approved: isApprovedSort
|
||||
? { icon: <MdOutlineLiveTv size={16} color="white" />, text: t("live") }
|
||||
: isFeature
|
||||
? { icon: <BiBadgeCheck size={16} color="white" />, text: t("featured") }
|
||||
: { icon: <MdOutlineLiveTv size={16} color="white" />, text: t("live") },
|
||||
|
||||
review: {
|
||||
icon: <IoTimerOutline size={16} color="white" />,
|
||||
text: t("review"),
|
||||
},
|
||||
"permanent rejected": {
|
||||
icon: <RxCross2 size={16} color="white" />,
|
||||
text: t("permanentRejected"),
|
||||
bg: "bg-red-600",
|
||||
},
|
||||
"soft rejected": {
|
||||
icon: <RxCross2 size={16} color="white" />,
|
||||
text: t("softRejected"),
|
||||
bg: "bg-red-500",
|
||||
},
|
||||
inactive: {
|
||||
icon: <MdAirplanemodeInactive size={16} color="white" />,
|
||||
text: t("deactivate"),
|
||||
bg: "bg-gray-500",
|
||||
},
|
||||
"sold out": {
|
||||
icon: <MdOutlineSell size={16} color="white" />,
|
||||
text: isJobCategory ? t("positionFilled") : t("soldOut"),
|
||||
bg: "bg-yellow-600",
|
||||
},
|
||||
resubmitted: {
|
||||
icon: <MdOutlineDone size={16} color="white" />,
|
||||
text: t("resubmitted"),
|
||||
bg: "bg-green-600",
|
||||
},
|
||||
expired: {
|
||||
icon: <RiPassExpiredLine size={16} color="white" />,
|
||||
text: t("expired"),
|
||||
bg: "bg-gray-700",
|
||||
},
|
||||
};
|
||||
|
||||
const { icon, text, bg = "bg-primary" } = statusComponents[status] || {};
|
||||
|
||||
if (!status) return null;
|
||||
|
||||
return (
|
||||
<div className={`flex items-center gap-1 ${bg} rounded-sm py-0.5 px-1`}>
|
||||
{icon}
|
||||
<span className="text-white text-sm text-ellipsis">{text}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default GetMyAdStatus;
|
||||
437
components/PagesComponent/MyAds/MyAds.jsx
Normal file
437
components/PagesComponent/MyAds/MyAds.jsx
Normal file
@@ -0,0 +1,437 @@
|
||||
"use client";
|
||||
import { t } from "@/utils";
|
||||
import { CgArrowsExchangeAltV } from "react-icons/cg";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
import AdsCard from "./MyAdsCard.jsx";
|
||||
import { deleteItemApi, getMyItemsApi, renewItemApi } from "@/utils/api";
|
||||
import { useSelector } from "react-redux";
|
||||
import ProductCardSkeleton from "@/components/Common/ProductCardSkeleton.jsx";
|
||||
import NoData from "@/components/EmptyStates/NoData";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
CurrentLanguageData,
|
||||
getIsRtl,
|
||||
} from "@/redux/reducer/languageSlice.js";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import ReusableAlertDialog from "@/components/Common/ReusableAlertDialog.jsx";
|
||||
import { toast } from "sonner";
|
||||
import ChoosePackageModal from "./ChoosePackageModal.jsx";
|
||||
import { getIsFreAdListing } from "@/redux/reducer/settingSlice.js";
|
||||
|
||||
const MyAds = () => {
|
||||
const CurrentLanguage = useSelector(CurrentLanguageData);
|
||||
const searchParams = useSearchParams();
|
||||
const isRTL = useSelector(getIsRtl);
|
||||
|
||||
const sortValue = searchParams.get("sort") || "new-to-old";
|
||||
const status = searchParams.get("status") || "all";
|
||||
|
||||
const [totalAdsCount, setTotalAdsCount] = useState(0);
|
||||
const [MyItems, setMyItems] = useState([]);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [lastPage, setLastPage] = useState(1);
|
||||
const [IsLoading, setIsLoading] = useState(true);
|
||||
const [IsLoadMore, setIsLoadMore] = useState(false);
|
||||
|
||||
const isFreeAdListing = useSelector(getIsFreAdListing);
|
||||
const [ItemPackages, setItemPackages] = useState([]);
|
||||
const [renewIds, setRenewIds] = useState([]);
|
||||
|
||||
const [selectedIds, setSelectedIds] = useState([]);
|
||||
const [IsDeleting, setIsDeleting] = useState(false);
|
||||
const [IsDeleteDialog, setIsDeleteDialog] = useState(false);
|
||||
|
||||
const [IsChoosePackage, setIsChoosePackage] = useState(false);
|
||||
const [selectedPackageId, setSelectedPackageId] = useState("");
|
||||
const [isRenewingAd, setIsRenewingAd] = useState(false);
|
||||
|
||||
// Filter expired ads and check if selection is allowed
|
||||
const expiredAds = MyItems.filter((item) => item.status === "expired");
|
||||
const canMultiSelect = expiredAds.length > 1;
|
||||
|
||||
const getMyItemsData = async (page = 1) => {
|
||||
try {
|
||||
const params = {
|
||||
page,
|
||||
sort_by: sortValue,
|
||||
};
|
||||
if (status !== "all") {
|
||||
params.status = status;
|
||||
}
|
||||
|
||||
// Set loading states based on page
|
||||
if (page > 1) {
|
||||
setIsLoadMore(true);
|
||||
} else {
|
||||
setIsLoading(true);
|
||||
}
|
||||
|
||||
const res = await getMyItemsApi.getMyItems(params);
|
||||
const data = res?.data;
|
||||
if (data?.error === false) {
|
||||
setTotalAdsCount(data?.data?.total);
|
||||
page > 1
|
||||
? setMyItems((prevData) => [...prevData, ...data?.data?.data])
|
||||
: setMyItems(data?.data?.data);
|
||||
setCurrentPage(data?.data?.current_page);
|
||||
setLastPage(data?.data?.last_page);
|
||||
} else {
|
||||
console.log("Error in response: ", data.message);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
setIsLoadMore(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
getMyItemsData(1);
|
||||
}, [sortValue, status, CurrentLanguage?.id]);
|
||||
|
||||
const updateURLParams = (key, value) => {
|
||||
const params = new URLSearchParams(searchParams);
|
||||
params.set(key, value);
|
||||
window.history.pushState(null, "", `?${params.toString()}`);
|
||||
};
|
||||
|
||||
const handleSortChange = (value) => {
|
||||
updateURLParams("sort", value);
|
||||
};
|
||||
|
||||
const handleStatusChange = (value) => {
|
||||
updateURLParams("status", value);
|
||||
};
|
||||
|
||||
const handleAdSelection = (adId) => {
|
||||
const ad = MyItems.find((item) => item.id === adId);
|
||||
if (ad?.status !== "expired") return;
|
||||
|
||||
setRenewIds((prev) => {
|
||||
if (prev.includes(adId)) {
|
||||
return prev.filter((id) => id !== adId);
|
||||
} else {
|
||||
return [...prev, adId];
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleRemove = async () => {
|
||||
if (selectedIds.length === 0) return;
|
||||
try {
|
||||
setIsDeleting(true);
|
||||
const payload = { item_ids: selectedIds.join(",") };
|
||||
// Call API
|
||||
const res = await deleteItemApi.deleteItem(payload);
|
||||
|
||||
// Handle response
|
||||
if (res?.data?.error === false) {
|
||||
toast.success(res?.data?.message);
|
||||
setIsDeleteDialog(false);
|
||||
setSelectedIds([]);
|
||||
setRenewIds([]);
|
||||
await getMyItemsData(1);
|
||||
} else {
|
||||
toast.error(res?.data?.message);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
} finally {
|
||||
setIsDeleting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const renewAds = async ({ ids, packageId }) => {
|
||||
try {
|
||||
setIsRenewingAd(true);
|
||||
let payload = {};
|
||||
if (Array.isArray(ids)) {
|
||||
payload = {
|
||||
item_ids: ids.join(","),
|
||||
...(isFreeAdListing ? {} : { package_id: packageId }),
|
||||
};
|
||||
} else {
|
||||
payload = {
|
||||
item_ids: ids,
|
||||
...(isFreeAdListing ? {} : { package_id: packageId }),
|
||||
};
|
||||
}
|
||||
|
||||
const res = await renewItemApi.renewItem(payload);
|
||||
|
||||
if (res?.data?.error === false) {
|
||||
toast.success(res?.data?.message);
|
||||
setIsChoosePackage(false);
|
||||
setRenewIds([]);
|
||||
await getMyItemsData(1);
|
||||
} else {
|
||||
toast.error(res?.data?.message);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
setIsRenewingAd(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRenew = (ids) => {
|
||||
const idsToRenew = Array.isArray(ids) ? ids : renewIds;
|
||||
|
||||
if (isFreeAdListing) {
|
||||
renewAds({ ids: idsToRenew });
|
||||
} else {
|
||||
if (!selectedPackageId) {
|
||||
toast.error(t("pleaseSelectPackage"));
|
||||
return;
|
||||
}
|
||||
|
||||
const subPackage = ItemPackages.find(
|
||||
(p) => Number(p.id) === Number(selectedPackageId)
|
||||
);
|
||||
if (!subPackage?.is_active) {
|
||||
toast.error(t("purchasePackageFirst"));
|
||||
navigate("/user-subscription");
|
||||
return;
|
||||
}
|
||||
renewAds({ ids: idsToRenew, packageId: selectedPackageId });
|
||||
}
|
||||
};
|
||||
|
||||
// Handle context menu actions
|
||||
const handleContextMenuAction = (action, adId) => {
|
||||
const ad = MyItems.find((item) => item.id === adId);
|
||||
|
||||
switch (action) {
|
||||
case "select":
|
||||
// Only allow selection for expired ads
|
||||
if (ad && ad.status === "expired") {
|
||||
handleAdSelection(adId);
|
||||
}
|
||||
break;
|
||||
case "renew":
|
||||
if (isFreeAdListing) {
|
||||
handleRenew([adId]);
|
||||
} else {
|
||||
setRenewIds([adId]);
|
||||
setIsChoosePackage(true);
|
||||
}
|
||||
break;
|
||||
case "delete":
|
||||
setSelectedIds([adId]); // single ad
|
||||
setIsDeleteDialog(true);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelectAll = () => {
|
||||
if (renewIds.length === expiredAds.length) {
|
||||
setRenewIds([]);
|
||||
} else {
|
||||
setRenewIds(expiredAds.map((item) => item.id));
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancelSelection = () => setRenewIds([]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col gap-2 md:flex-row md:items-center md:justify-between py-2 px-4 bg-muted rounded-lg">
|
||||
<h1 className="font-semibold">
|
||||
{t("totalAds")} {totalAdsCount}
|
||||
</h1>
|
||||
|
||||
<div className="flex flex-wrap sm:flex-nowrap items-center gap-2">
|
||||
<div className="flex items-center gap-1">
|
||||
<CgArrowsExchangeAltV size={25} />
|
||||
<span className="whitespace-nowrap">{t("sortBy")}</span>
|
||||
</div>
|
||||
<Select value={sortValue} onValueChange={handleSortChange}>
|
||||
<SelectTrigger className="bg-transparent border-black/23 whitespace-nowrap">
|
||||
<SelectValue placeholder="Sort by" />
|
||||
</SelectTrigger>
|
||||
<SelectContent align={isRTL ? "start" : "end"}>
|
||||
<SelectGroup>
|
||||
<SelectItem value="new-to-old">
|
||||
{t("newestToOldest")}
|
||||
</SelectItem>
|
||||
<SelectItem value="old-to-new">
|
||||
{t("oldestToNewest")}
|
||||
</SelectItem>
|
||||
<SelectItem value="price-high-to-low">
|
||||
{t("priceHighToLow")}
|
||||
</SelectItem>
|
||||
<SelectItem value="price-low-to-high">
|
||||
{t("priceLowToHigh")}
|
||||
</SelectItem>
|
||||
<SelectItem value="popular_items">{t("popular")}</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<Select value={status} onValueChange={handleStatusChange}>
|
||||
<SelectTrigger className="bg-transparent border-black/23">
|
||||
<SelectValue placeholder="Status" />
|
||||
</SelectTrigger>
|
||||
<SelectContent align={isRTL ? "start" : "end"}>
|
||||
<SelectGroup>
|
||||
<SelectItem value="all">{t("all")}</SelectItem>
|
||||
<SelectItem value="review">{t("review")}</SelectItem>
|
||||
<SelectItem value="approved">{t("live")}</SelectItem>
|
||||
<SelectItem value="soft rejected">
|
||||
{t("softRejected")}
|
||||
</SelectItem>
|
||||
<SelectItem value="permanent rejected">
|
||||
{t("permanentRejected")}
|
||||
</SelectItem>
|
||||
<SelectItem value="inactive">{t("deactivate")}</SelectItem>
|
||||
<SelectItem value="featured">{t("featured")}</SelectItem>
|
||||
<SelectItem value="sold out">{t("soldOut")}</SelectItem>
|
||||
<SelectItem value="resubmitted">{t("resubmitted")}</SelectItem>
|
||||
<SelectItem value="expired">{t("expired")}</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-[30px] text-sm text-destructive">
|
||||
<span>{t("expiredAdsNote")}</span>
|
||||
</div>
|
||||
|
||||
{/* Selection controls - only show when there are expired ads and at least one is selected */}
|
||||
{canMultiSelect && renewIds.length > 0 && (
|
||||
<div className="flex items-center justify-between mt-[30px]">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
checked={renewIds.length === expiredAds.length}
|
||||
onCheckedChange={handleSelectAll}
|
||||
className="data-[state=checked]:bg-primary data-[state=checked]:border-primary"
|
||||
/>
|
||||
<span className="text-sm font-medium">{t("selectAll")}</span>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-sm">
|
||||
{renewIds.length} {renewIds.length === 1 ? t("ad") : t("ads")}{" "}
|
||||
{t("selected")}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 mt-[30px] xl:grid-cols-3 gap-3 sm:gap-6">
|
||||
{IsLoading ? (
|
||||
[...Array(6)].map((item, index) => (
|
||||
<ProductCardSkeleton key={index} />
|
||||
))
|
||||
) : MyItems && MyItems?.length > 0 ? (
|
||||
MyItems.map((item) => (
|
||||
<AdsCard
|
||||
key={item?.id}
|
||||
data={item}
|
||||
isApprovedSort={sortValue === "approved"}
|
||||
isSelected={renewIds.includes(item?.id)}
|
||||
isSelectable={renewIds.length > 0 && item.status === "expired"}
|
||||
onSelectionToggle={() => handleAdSelection(item?.id)}
|
||||
onContextMenuAction={(action) =>
|
||||
handleContextMenuAction(action, item?.id)
|
||||
}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<div className="col-span-full">
|
||||
<NoData name={t("advertisement")} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{currentPage < lastPage && (
|
||||
<div className="text-center mt-6">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="text-sm sm:text-base text-primary w-[256px]"
|
||||
disabled={IsLoading || IsLoadMore}
|
||||
onClick={() => getMyItemsData(currentPage + 1)}
|
||||
>
|
||||
{IsLoadMore ? t("loading") : t("loadMore")}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Action buttons for selected ads - show at bottom */}
|
||||
{renewIds.length > 0 && (
|
||||
<div className="mt-[30px]">
|
||||
<div className="flex items-center justify-end gap-3">
|
||||
<Button
|
||||
onClick={handleCancelSelection}
|
||||
className="bg-black text-white"
|
||||
>
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (renewIds.length === 0) return; // no selection
|
||||
setSelectedIds([...renewIds]); // copy renewIds to selectedIds
|
||||
setIsDeleteDialog(true);
|
||||
}}
|
||||
className="bg-destructive text-white"
|
||||
>
|
||||
{t("remove")}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (isFreeAdListing) {
|
||||
handleRenew(); // directly renew
|
||||
} else {
|
||||
setIsChoosePackage(true);
|
||||
}
|
||||
}}
|
||||
disabled={isRenewingAd}
|
||||
className="bg-primary text-white"
|
||||
>
|
||||
{isRenewingAd ? t("loading") : t("renew")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<ChoosePackageModal
|
||||
key={IsChoosePackage}
|
||||
selectedPackageId={selectedPackageId}
|
||||
setSelectedPackageId={setSelectedPackageId}
|
||||
ItemPackages={ItemPackages}
|
||||
setItemPackages={setItemPackages}
|
||||
IsChoosePackage={IsChoosePackage}
|
||||
setIsChoosePackage={setIsChoosePackage}
|
||||
handleRenew={handleRenew}
|
||||
isRenewingAd={isRenewingAd}
|
||||
/>
|
||||
<ReusableAlertDialog
|
||||
open={IsDeleteDialog}
|
||||
onCancel={() => setIsDeleteDialog(false)}
|
||||
onConfirm={handleRemove}
|
||||
title={t("areYouSure")}
|
||||
description={
|
||||
selectedIds.length === 1
|
||||
? t("areYouSureToDeleteAd")
|
||||
: t("areYouSureToDeleteAds")
|
||||
}
|
||||
cancelText={t("cancel")}
|
||||
confirmText={t("yes")}
|
||||
confirmDisabled={IsDeleting}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default MyAds;
|
||||
162
components/PagesComponent/MyAds/MyAdsCard.jsx
Normal file
162
components/PagesComponent/MyAds/MyAdsCard.jsx
Normal file
@@ -0,0 +1,162 @@
|
||||
import CustomLink from "@/components/Common/CustomLink";
|
||||
import { BiHeart } from "react-icons/bi";
|
||||
import { RxEyeOpen } from "react-icons/rx";
|
||||
import { t } from "@/utils";
|
||||
import CustomImage from "@/components/Common/CustomImage";
|
||||
import GetMyAdStatus from "./GetMyAdStatus";
|
||||
import {
|
||||
ContextMenu,
|
||||
ContextMenuContent,
|
||||
ContextMenuItem,
|
||||
ContextMenuTrigger,
|
||||
} from "@/components/ui/context-menu";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { RotateCcw, Trash2, CheckSquare } from "lucide-react";
|
||||
|
||||
const MyAdsCard = ({
|
||||
data,
|
||||
isApprovedSort,
|
||||
isSelected = false,
|
||||
isSelectable = false,
|
||||
onSelectionToggle,
|
||||
onContextMenuAction,
|
||||
}) => {
|
||||
const isJobCategory = Number(data?.category?.is_job_category) === 1;
|
||||
const isAdminEdited = Number(data?.is_edited_by_admin) === 1;
|
||||
const translated_item = data?.translated_item;
|
||||
|
||||
const isHidePrice = isJobCategory
|
||||
? !data?.formatted_salary_range
|
||||
: !data?.formatted_price;
|
||||
|
||||
const status = data?.status;
|
||||
const isExpired = status === "expired";
|
||||
|
||||
const price = isJobCategory
|
||||
? data?.formatted_salary_range
|
||||
: data?.formatted_price;
|
||||
|
||||
// Card content JSX to avoid duplication
|
||||
const cardContent = (
|
||||
<div
|
||||
className={`relative border flex flex-col gap-2 rounded-xl p-2 hover:shadow-md transition-all duration-200 ${
|
||||
isSelected ? "ring-2 ring-primary bg-primary/5" : ""
|
||||
}`}
|
||||
>
|
||||
{/* Selection checkbox - only show in selection mode */}
|
||||
{isSelectable && (
|
||||
<div className="absolute top-2 left-2 z-10">
|
||||
<Checkbox
|
||||
checked={isSelected}
|
||||
onCheckedChange={onSelectionToggle}
|
||||
className="bg-white shadow-sm border-2 border-primary data-[state=checked]:bg-primary data-[state=checked]:border-primary"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Main card content */}
|
||||
<CustomLink
|
||||
href={`/my-listing/${data?.slug}`}
|
||||
className="flex flex-col gap-2"
|
||||
onClick={(e) => {
|
||||
if (isSelectable) {
|
||||
e.preventDefault();
|
||||
onSelectionToggle();
|
||||
} else {
|
||||
// For navigation, ensure the event propagates properly
|
||||
// Don't prevent default or stop propagation for normal clicks
|
||||
}
|
||||
}}
|
||||
>
|
||||
<CustomImage
|
||||
src={data?.image}
|
||||
width={220}
|
||||
height={220}
|
||||
alt={data?.image}
|
||||
className="w-full h-auto aspect-square rounded-sm object-cover"
|
||||
/>
|
||||
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
{status && (
|
||||
<GetMyAdStatus
|
||||
status={status}
|
||||
isApprovedSort={isApprovedSort}
|
||||
isFeature={data?.is_feature}
|
||||
isJobCategory={isJobCategory}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isAdminEdited && (
|
||||
<div className="py-1 px-2 bg-red-400/15 rounded-sm text-destructive text-sm">
|
||||
{t("adminEdited")}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!isHidePrice && (
|
||||
<p className="font-medium line-clamp-1">
|
||||
{translated_item?.name || data?.name}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="space-between gap-1">
|
||||
{isHidePrice ? (
|
||||
<p className="font-medium line-clamp-1">
|
||||
{translated_item?.name || data?.name}
|
||||
</p>
|
||||
) : (
|
||||
<p
|
||||
className="font-semibold text-lg text-balance break-all line-clamp-2"
|
||||
title={price}
|
||||
>
|
||||
{price}
|
||||
</p>
|
||||
)}
|
||||
<div className="flex items-center gap-1 text-xs">
|
||||
<div className="flex items-center gap-1">
|
||||
<RxEyeOpen size={14} className="text-black/60" />
|
||||
<span>{data?.clicks}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<BiHeart size={14} className="text-black/60" />
|
||||
<span>{data?.total_likes}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CustomLink>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<ContextMenu modal={false}>
|
||||
<ContextMenuTrigger asChild disabled={!isExpired}>
|
||||
{cardContent}
|
||||
</ContextMenuTrigger>
|
||||
<ContextMenuContent>
|
||||
<ContextMenuItem
|
||||
onClick={() => onContextMenuAction("select")}
|
||||
className="flex items-center gap-2 cursor-pointer"
|
||||
>
|
||||
<CheckSquare className="size-4" />
|
||||
{isSelected ? "Deselect" : "Select"}
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem
|
||||
onClick={() => onContextMenuAction("renew")}
|
||||
className="flex items-center gap-2 cursor-pointer"
|
||||
>
|
||||
<RotateCcw className="size-4 text-primary" />
|
||||
<span className="text-primary">{t("renew")}</span>
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem
|
||||
onClick={() => onContextMenuAction("delete")}
|
||||
className="flex items-center gap-2 text-destructive focus:text-destructive cursor-pointer"
|
||||
>
|
||||
<Trash2 className="size-4" />
|
||||
{t("remove")}
|
||||
</ContextMenuItem>
|
||||
</ContextMenuContent>
|
||||
</ContextMenu>
|
||||
);
|
||||
};
|
||||
|
||||
export default MyAdsCard;
|
||||
Reference in New Issue
Block a user