classify web
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
} from "@/components/ui/dialog";
|
||||
import trueGif from "../../../public/assets/true.gif";
|
||||
import CustomLink from "@/components/Common/CustomLink";
|
||||
import { t } from "@/utils";
|
||||
import CustomImage from "@/components/Common/CustomImage";
|
||||
|
||||
const AdEditSuccessModal = ({
|
||||
openSuccessModal,
|
||||
setOpenSuccessModal,
|
||||
createdAdSlug,
|
||||
}) => {
|
||||
const closeSuccessModal = () => {
|
||||
setOpenSuccessModal(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={openSuccessModal} onOpenChange={closeSuccessModal}>
|
||||
<DialogContent
|
||||
className="[&>button]:hidden lgmax-w-[100px]"
|
||||
onInteractOutside={(e) => {
|
||||
e.preventDefault();
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-col items-center justify-center gap-4">
|
||||
<div className="flex flex-col justify-center items-center">
|
||||
<CustomImage
|
||||
src={trueGif}
|
||||
alt="success"
|
||||
height={176}
|
||||
width={176}
|
||||
className="h-44 w-44"
|
||||
/>
|
||||
<h2 className="text-3xl font-semibold">{t("adEditedSuccess")}</h2>
|
||||
</div>
|
||||
<CustomLink
|
||||
href={`/my-listing/${createdAdSlug}`}
|
||||
className="py-3 px-6 bg-primary text-white rounded-md"
|
||||
>
|
||||
{t("viewAd")}
|
||||
</CustomLink>
|
||||
<CustomLink href="/" className="">
|
||||
{t("backToHome")}
|
||||
</CustomLink>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdEditSuccessModal;
|
||||
177
components/PagesComponent/EditListing/EditComponentFour.jsx
Normal file
177
components/PagesComponent/EditListing/EditComponentFour.jsx
Normal file
@@ -0,0 +1,177 @@
|
||||
import { useState } from "react";
|
||||
import { BiMapPin } from "react-icons/bi";
|
||||
import { FaLocationCrosshairs } from "react-icons/fa6";
|
||||
import { IoLocationOutline } from "react-icons/io5";
|
||||
import ManualAddress from "../AdsListing/ManualAddress";
|
||||
import { t } from "@/utils";
|
||||
import { getIsPaidApi } from "@/redux/reducer/settingSlice";
|
||||
import { useSelector } from "react-redux";
|
||||
import dynamic from "next/dynamic";
|
||||
import { CurrentLanguageData } from "@/redux/reducer/languageSlice";
|
||||
import LandingAdEditSearchAutocomplete from "@/components/Location/LandingAdEditSearchAutocomplete";
|
||||
import { getIsBrowserSupported } from "@/redux/reducer/locationSlice";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import useGetLocation from "@/components/Layout/useGetLocation";
|
||||
|
||||
const MapComponent = dynamic(() => import("@/components/Common/MapComponent"), {
|
||||
ssr: false,
|
||||
loading: () => <div className="w-full h-[400px] bg-gray-100 rounded-lg" />,
|
||||
});
|
||||
|
||||
const EditComponentFour = ({
|
||||
location,
|
||||
setLocation,
|
||||
handleFullSubmission,
|
||||
isAdPlaced,
|
||||
handleGoBack,
|
||||
}) => {
|
||||
const isBrowserSupported = useSelector(getIsBrowserSupported);
|
||||
const CurrentLanguage = useSelector(CurrentLanguageData);
|
||||
const [showManualAddress, setShowManualAddress] = useState();
|
||||
const [IsGettingCurrentLocation, setIsGettingCurrentLocation] =
|
||||
useState(false);
|
||||
const IsPaidApi = useSelector(getIsPaidApi);
|
||||
const { fetchLocationData } = useGetLocation();
|
||||
|
||||
const getCurrentLocation = async () => {
|
||||
if (navigator.geolocation) {
|
||||
setIsGettingCurrentLocation(true);
|
||||
navigator.geolocation.getCurrentPosition(
|
||||
async (position) => {
|
||||
try {
|
||||
const { latitude, longitude } = position.coords;
|
||||
const data = await fetchLocationData({
|
||||
lat: latitude,
|
||||
lng: longitude,
|
||||
});
|
||||
setLocation(data);
|
||||
} catch (error) {
|
||||
console.error("Error fetching location data:", error);
|
||||
toast.error(t("errorOccurred"));
|
||||
} finally {
|
||||
setIsGettingCurrentLocation(false);
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
toast.error(t("locationNotGranted"));
|
||||
setIsGettingCurrentLocation(false);
|
||||
}
|
||||
);
|
||||
} else {
|
||||
toast.error(t("geoLocationNotSupported"));
|
||||
}
|
||||
};
|
||||
|
||||
const getLocationWithMap = async (pos) => {
|
||||
try {
|
||||
const data = await fetchLocationData(pos);
|
||||
setLocation(data);
|
||||
} catch (error) {
|
||||
console.error("Error fetching location data:", error);
|
||||
toast.error(t("errorOccurred"));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-8">
|
||||
<div className="flex flex-col md:flex-row items-start md:items-center justify-between gap-3">
|
||||
<h5 className="text-xl font-medium">{t("addLocation")}</h5>
|
||||
<div className="flex items-center gap-2 border rounded-md w-full md:w-96 min-h-[42px]">
|
||||
<LandingAdEditSearchAutocomplete
|
||||
saveOnSuggestionClick={false}
|
||||
setSelectedLocation={setLocation}
|
||||
/>
|
||||
{isBrowserSupported && (
|
||||
<button
|
||||
onClick={getCurrentLocation}
|
||||
disabled={IsGettingCurrentLocation}
|
||||
className="bg-primary p-2 text-white gap-2 flex items-center rounded-md h-10"
|
||||
>
|
||||
<span>
|
||||
{IsGettingCurrentLocation ? (
|
||||
<Loader2 className="size-3.5 animate-spin" />
|
||||
) : (
|
||||
<FaLocationCrosshairs size={16} />
|
||||
)}
|
||||
</span>
|
||||
<span className="whitespace-nowrap hidden md:inline">
|
||||
{IsGettingCurrentLocation ? t("loading") : t("locateMe")}
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-8 flex-col">
|
||||
<MapComponent
|
||||
location={location}
|
||||
getLocationWithMap={getLocationWithMap}
|
||||
/>
|
||||
<div className="flex items-center gap-3 bg-muted rounded-lg p-4 ">
|
||||
<div className="p-5 rounded-md bg-white">
|
||||
<BiMapPin className="text-primary" size={32} />
|
||||
</div>
|
||||
<span className="flex flex-col gap-1">
|
||||
<h6 className="font-medium">{t("address")}</h6>
|
||||
{location?.address_translated || location?.formattedAddress ? (
|
||||
<p>
|
||||
{location?.address_translated || location?.formattedAddress}
|
||||
</p>
|
||||
) : (
|
||||
t("addYourAddress")
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!IsPaidApi && (
|
||||
<>
|
||||
<div className="relative flex items-center justify-center ">
|
||||
<div className="absolute top-1/2 left-0 right-0 h-px bg-[#d3d3d3]"></div>
|
||||
<div className="relative bg-muted text-black text-base font-medium rounded-full w-12 h-12 flex items-center justify-center uppercase">
|
||||
{t("or")}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3 items-center justify-center ">
|
||||
<p className="text-xl font-semibold">
|
||||
{t("whatLocAdYouSelling")}
|
||||
</p>
|
||||
<button
|
||||
className="p-2 flex items-center gap-2 border rounded-md font-medium"
|
||||
onClick={() => setShowManualAddress(true)}
|
||||
>
|
||||
<IoLocationOutline size={20} />
|
||||
{t("addLocation")}
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex justify-end gap-3">
|
||||
<button
|
||||
className="bg-black text-white px-4 py-2 rounded-md text-xl font-light"
|
||||
onClick={handleGoBack}
|
||||
>
|
||||
{t("back")}
|
||||
</button>
|
||||
<button
|
||||
className="bg-primary text-white px-4 py-2 rounded-md text-xl font-light0 disabled:bg-gray-500"
|
||||
disabled={isAdPlaced}
|
||||
onClick={handleFullSubmission}
|
||||
>
|
||||
{isAdPlaced ? t("posting") : t("postNow")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<ManualAddress
|
||||
key={showManualAddress}
|
||||
showManualAddress={showManualAddress}
|
||||
setShowManualAddress={setShowManualAddress}
|
||||
setLocation={setLocation}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditComponentFour;
|
||||
289
components/PagesComponent/EditListing/EditComponentOne.jsx
Normal file
289
components/PagesComponent/EditListing/EditComponentOne.jsx
Normal file
@@ -0,0 +1,289 @@
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { getIsRtl } from "@/redux/reducer/languageSlice";
|
||||
import {
|
||||
getCurrencyIsoCode,
|
||||
getCurrencyPosition,
|
||||
getCurrencySymbol,
|
||||
} from "@/redux/reducer/settingSlice";
|
||||
import { generateSlug, t } from "@/utils";
|
||||
import PhoneInput from "react-phone-input-2";
|
||||
import { useSelector } from "react-redux";
|
||||
const EditComponentOne = ({
|
||||
setTranslations,
|
||||
current,
|
||||
langId,
|
||||
defaultLangId,
|
||||
handleDetailsSubmit,
|
||||
is_job_category,
|
||||
isPriceOptional,
|
||||
currencies,
|
||||
}) => {
|
||||
const currencyPosition = useSelector(getCurrencyPosition);
|
||||
const currencySymbol = useSelector(getCurrencySymbol);
|
||||
const currencyIsoCode = useSelector(getCurrencyIsoCode);
|
||||
const isRTL = useSelector(getIsRtl);
|
||||
const selectedCurrency = currencies?.find(
|
||||
(curr) => curr?.id === current?.currency_id
|
||||
);
|
||||
|
||||
// Use selected currency's symbol and position, or fallback to Redux settings
|
||||
const displaySymbol = selectedCurrency?.symbol || currencySymbol;
|
||||
const displayPosition = selectedCurrency?.position || currencyPosition;
|
||||
|
||||
const placeholderLabel =
|
||||
displayPosition === "right" ? `00 ${displaySymbol}` : `${displaySymbol} 00`;
|
||||
|
||||
const handleField = (field) => (e) => {
|
||||
const value = e.target.value;
|
||||
setTranslations((prev) => {
|
||||
const updatedLangData = {
|
||||
...prev[langId],
|
||||
[field]: value,
|
||||
};
|
||||
|
||||
// ✅ Only auto-generate slug if default language and field is title
|
||||
if (field === "name" && langId === defaultLangId) {
|
||||
updatedLangData.slug = generateSlug(value);
|
||||
}
|
||||
|
||||
return {
|
||||
...prev,
|
||||
[langId]: updatedLangData,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const handlePhoneChange = (value, data) => {
|
||||
const dial = data?.dialCode || ""; // Dial code like "91", "1"
|
||||
const iso2 = data?.countryCode || ""; // Region code like "in", "us", "ae"
|
||||
setTranslations((prev) => {
|
||||
const pureMobile = value.startsWith(dial)
|
||||
? value.slice(dial.length)
|
||||
: value;
|
||||
return {
|
||||
...prev,
|
||||
[langId]: {
|
||||
...prev[langId],
|
||||
contact: pureMobile,
|
||||
country_code: dial,
|
||||
region_code: iso2,
|
||||
},
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const handleCurrencyChange = (currencyId) => {
|
||||
setTranslations((prev) => ({
|
||||
...prev,
|
||||
[langId]: {
|
||||
...prev[langId],
|
||||
currency_id: Number(currencyId),
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label
|
||||
htmlFor="title"
|
||||
className={langId === defaultLangId ? "requiredInputLabel" : ""}
|
||||
>
|
||||
{t("title")}
|
||||
</Label>
|
||||
<Input
|
||||
type="text"
|
||||
name="title"
|
||||
id="title"
|
||||
placeholder={t("enterTitle")}
|
||||
value={current.name || ""}
|
||||
onChange={handleField("name")}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label
|
||||
htmlFor="description"
|
||||
className={langId === defaultLangId ? "requiredInputLabel" : ""}
|
||||
>
|
||||
{t("description")}
|
||||
</Label>
|
||||
<Textarea
|
||||
name="description"
|
||||
id="description"
|
||||
placeholder={t("enterDescription")}
|
||||
value={current.description || ""}
|
||||
onChange={handleField("description")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="currency">{t("currency")}</Label>
|
||||
{
|
||||
currencies?.length > 0 ?
|
||||
<Select
|
||||
value={current?.currency_id?.toString()}
|
||||
onValueChange={handleCurrencyChange}
|
||||
dir={isRTL ? "rtl" : "ltr"}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder={t("currency")} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
{/* <SelectLabel>Currencies</SelectLabel> */}
|
||||
{currencies?.map((currency) => (
|
||||
<SelectItem
|
||||
key={currency.id}
|
||||
value={currency.id.toString()}
|
||||
dir={isRTL ? "rtl" : "ltr"}
|
||||
>
|
||||
{currency.iso_code} ({currency.symbol})
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
:
|
||||
<Select
|
||||
value={currencyIsoCode} // ✅ same default value you already have
|
||||
disabled
|
||||
dir={isRTL ? "rtl" : "ltr"}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
|
||||
{/* Required for RTL */}
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectItem value={currencyIsoCode}>
|
||||
{currencyIsoCode} ({currencySymbol})
|
||||
</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
}
|
||||
</div>
|
||||
{langId === defaultLangId && (
|
||||
<>
|
||||
{is_job_category ? (
|
||||
<>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="min_salary">{t("salaryMin")}</Label>
|
||||
<Input
|
||||
type="number"
|
||||
name="min_salary"
|
||||
id="min_salary"
|
||||
placeholder={placeholderLabel}
|
||||
value={current.min_salary || ""}
|
||||
onChange={handleField("min_salary")}
|
||||
min={0}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="max_salary">{t("salaryMax")}</Label>
|
||||
<Input
|
||||
type="number"
|
||||
name="max_salary"
|
||||
id="max_salary"
|
||||
placeholder={placeholderLabel}
|
||||
value={current.max_salary || ""}
|
||||
onChange={handleField("max_salary")}
|
||||
min={0}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label
|
||||
htmlFor="price"
|
||||
className={
|
||||
!isPriceOptional && langId === defaultLangId
|
||||
? "requiredInputLabel"
|
||||
: ""
|
||||
}
|
||||
>
|
||||
{t("price")}
|
||||
</Label>
|
||||
<Input
|
||||
type="number"
|
||||
name="price"
|
||||
id="price"
|
||||
placeholder={placeholderLabel}
|
||||
value={current.price || ""}
|
||||
onChange={handleField("price")}
|
||||
min={0}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label
|
||||
htmlFor="phonenumber"
|
||||
>
|
||||
{t("phoneNumber")}
|
||||
</Label>
|
||||
<PhoneInput
|
||||
country={process.env.NEXT_PUBLIC_DEFAULT_COUNTRY}
|
||||
value={`${current.country_code}${current.contact}`}
|
||||
onChange={(phone, data) => handlePhoneChange(phone, data)}
|
||||
inputProps={{
|
||||
name: "phonenumber",
|
||||
id: "phonenumber",
|
||||
}}
|
||||
enableLongNumbers
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="videoLink">{t("videoLink")}</Label>
|
||||
<Input
|
||||
type="text"
|
||||
name="videoLink"
|
||||
id="videoLink"
|
||||
placeholder={t("enterAdditionalLinks")}
|
||||
value={current.video_link || ""}
|
||||
onChange={handleField("video_link")}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="slug">
|
||||
{t("slug")}{" "}
|
||||
<span className="text-sm text-muted-foreground">
|
||||
({t("allowedSlug")})
|
||||
</span>
|
||||
</Label>
|
||||
<Input
|
||||
type="text"
|
||||
name="slug"
|
||||
id="slug"
|
||||
placeholder={t("enterSlug")}
|
||||
onChange={handleField("slug")}
|
||||
value={current.slug || ""}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="flex justify-end">
|
||||
<button
|
||||
className="bg-primary text-white px-4 py-2 rounded-md text-xl"
|
||||
onClick={handleDetailsSubmit}
|
||||
>
|
||||
{t("next")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditComponentOne;
|
||||
298
components/PagesComponent/EditListing/EditComponentThree.jsx
Normal file
298
components/PagesComponent/EditListing/EditComponentThree.jsx
Normal file
@@ -0,0 +1,298 @@
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { IoInformationCircleOutline } from "react-icons/io5";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { useDropzone } from "react-dropzone";
|
||||
import { HiOutlineUpload } from "react-icons/hi";
|
||||
import { MdClose } from "react-icons/md";
|
||||
import { t } from "@/utils";
|
||||
import CustomImage from "@/components/Common/CustomImage";
|
||||
|
||||
const EditComponentThree = ({
|
||||
uploadedImages,
|
||||
setUploadedImages,
|
||||
OtherImages,
|
||||
setOtherImages,
|
||||
handleImageSubmit,
|
||||
handleGoBack,
|
||||
setDeleteImagesId,
|
||||
}) => {
|
||||
|
||||
|
||||
const onDrop = useCallback((acceptedFiles) => {
|
||||
if (acceptedFiles.length == 0) {
|
||||
toast.error(t("wrongFile"));
|
||||
} else {
|
||||
setUploadedImages(acceptedFiles);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const { getRootProps, getInputProps, isDragActive } = useDropzone({
|
||||
onDrop,
|
||||
accept: {
|
||||
"image/jpeg": [".jpeg", ".jpg"],
|
||||
"image/png": [".png"],
|
||||
},
|
||||
multiple: false,
|
||||
});
|
||||
|
||||
const files = useMemo(() => {
|
||||
if (typeof uploadedImages === "string") {
|
||||
return (
|
||||
<div className="relative">
|
||||
<CustomImage
|
||||
width={591}
|
||||
height={350}
|
||||
className="rounded-2xl object-cover aspect-[591/350]"
|
||||
src={uploadedImages}
|
||||
alt="Uploaded Image"
|
||||
/>
|
||||
<div className="absolute top-2 left-2 flex gap-2 items-center">
|
||||
<button
|
||||
className="bg-white p-1 rounded-full"
|
||||
onClick={() => removeImage(0)}
|
||||
>
|
||||
<MdClose
|
||||
size={14}
|
||||
color="black"
|
||||
className="flex items-center justify-center rounded-full"
|
||||
/>
|
||||
</button>
|
||||
<div className="text-white flex flex-col">
|
||||
<span>{t("uploadedImage")}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
uploadedImages?.map((file, index) => (
|
||||
<div key={index} className="relative">
|
||||
<CustomImage
|
||||
width={591}
|
||||
height={350}
|
||||
className="rounded-2xl object-cover aspect-[591/350]"
|
||||
src={URL.createObjectURL(file)}
|
||||
alt={index}
|
||||
/>
|
||||
<div className="absolute top-2 left-2 flex gap-2 items-center">
|
||||
<button
|
||||
className="bg-white p-1 rounded-full"
|
||||
onClick={() => removeImage(index)}
|
||||
>
|
||||
<MdClose
|
||||
size={14}
|
||||
color="black"
|
||||
className="flex items-center justify-center rounded-full"
|
||||
/>
|
||||
</button>
|
||||
<div className="text-white text-xs flex flex-col">
|
||||
<span>{file.name}</span>
|
||||
<span>{Math.round(file.size / 1024)} KB</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)) || []
|
||||
);
|
||||
}
|
||||
}, [uploadedImages]);
|
||||
|
||||
const removeImage = (index) => {
|
||||
if (typeof uploadedImages === "string") {
|
||||
setUploadedImages([]);
|
||||
} else {
|
||||
setUploadedImages((prevImages) =>
|
||||
prevImages?.filter((_, i) => i !== index)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const onOtherDrop = useCallback(
|
||||
(acceptedFiles) => {
|
||||
const currentFilesCount = OtherImages.length; // Number of files already uploaded
|
||||
const remainingSlots = 5 - currentFilesCount; // How many more files can be uploaded
|
||||
|
||||
if (remainingSlots === 0) {
|
||||
// Show error if the limit has been reached
|
||||
toast.error(t("imageLimitExceeded"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (acceptedFiles.length > remainingSlots) {
|
||||
// Show error if the number of new files exceeds the remaining slots
|
||||
toast.error(
|
||||
t("youCanUpload") + " " + remainingSlots + " " + t("moreImages")
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Add the new files to the state
|
||||
setOtherImages((prevImages) => [...prevImages, ...acceptedFiles]);
|
||||
},
|
||||
[OtherImages]
|
||||
);
|
||||
|
||||
const {
|
||||
getRootProps: getRootOtheProps,
|
||||
getInputProps: getInputOtherProps,
|
||||
isDragActive: isDragOtherActive,
|
||||
} = useDropzone({
|
||||
onDrop: onOtherDrop,
|
||||
accept: {
|
||||
"image/jpeg": [".jpeg", ".jpg"],
|
||||
"image/png": [".png"],
|
||||
},
|
||||
multiple: true,
|
||||
});
|
||||
|
||||
const removeOtherImage = (index, file) => {
|
||||
setOtherImages((prevImages) => prevImages.filter((_, i) => i !== index));
|
||||
setDeleteImagesId((prevIds) => {
|
||||
const newId = file?.id;
|
||||
if (prevIds) {
|
||||
return `${prevIds},${newId}`;
|
||||
} else {
|
||||
return `${newId}`;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const filesOther = useMemo(
|
||||
() =>
|
||||
OtherImages &&
|
||||
OtherImages?.map((file, index) => (
|
||||
<div key={file.id || `${file?.name}-${file?.size}`} className="relative">
|
||||
<CustomImage
|
||||
width={591}
|
||||
height={350}
|
||||
className="rounded-2xl object-cover aspect-[591/350]"
|
||||
src={file.image ? file.image : URL.createObjectURL(file)}
|
||||
alt={index}
|
||||
/>
|
||||
<div className="absolute top-2 left-2 flex gap-2 items-center">
|
||||
<button
|
||||
className="bg-white p-1 rounded-full"
|
||||
onClick={() => removeOtherImage(index, file)}
|
||||
>
|
||||
<MdClose
|
||||
size={14}
|
||||
color="black"
|
||||
className="flex items-center justify-center rounded-full"
|
||||
/>
|
||||
</button>
|
||||
{
|
||||
(file?.name || file?.size) &&
|
||||
<div className="text-white text-xs flex flex-col">
|
||||
<span>{file.name}</span>
|
||||
<span>{Math.round(file.size / 1024)} KB</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)),
|
||||
[OtherImages]
|
||||
);
|
||||
return (
|
||||
<div className="flex flex-col gap-8">
|
||||
<div className="grid grid-col-1 md:grid-cols-2 gap-4">
|
||||
<div className="flex flex-col gap-2">
|
||||
<span className="requiredInputLabel text-sm">{t("mainPicture")}</span>
|
||||
<div className="border-2 border-dashed rounded-lg p-2">
|
||||
<div
|
||||
{...getRootProps()}
|
||||
className="flex flex-col min-h-[175px] items-center justify-center cursor-pointer"
|
||||
style={{ display: uploadedImages.length > 0 ? "none" : "" }}
|
||||
>
|
||||
<input {...getInputProps()} />
|
||||
{isDragActive ? (
|
||||
<span className="text-muted-foreground font-medium">
|
||||
{t("dropFiles")}
|
||||
</span>
|
||||
) : (
|
||||
<div className="flex flex-col items-center gap-2 text-center">
|
||||
<span className="text-muted-foreground">
|
||||
{t("dragFiles")}
|
||||
</span>
|
||||
<span className="text-muted-foreground">{t("or")}</span>
|
||||
<div className="flex items-center gap-2 text-primary">
|
||||
<HiOutlineUpload size={24} />
|
||||
<span className="font-medium">{t("upload")}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div>{files}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<span className="flex items-center gap-1 font-semibold text-sm">
|
||||
{t("otherPicture")}
|
||||
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span className="inline-flex">
|
||||
<IoInformationCircleOutline size={22} />
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
side="top"
|
||||
align="center"
|
||||
className="font-normal"
|
||||
>
|
||||
<p>{t("maxOtherImages")}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</span>
|
||||
<div className="border-2 border-dashed rounded-lg p-2">
|
||||
<div
|
||||
{...getRootOtheProps()}
|
||||
className="flex flex-col items-center justify-center min-h-[175px] cursor-pointer"
|
||||
style={{ display: OtherImages.length >= 5 ? "none" : "" }}
|
||||
>
|
||||
<input {...getInputOtherProps()} />
|
||||
{isDragOtherActive ? (
|
||||
<span className="text-primary font-medium">
|
||||
{t("dropFiles")}
|
||||
</span>
|
||||
) : (
|
||||
<div className="flex flex-col gap-2 items-center text-center">
|
||||
<span className="text-muted-foreground">
|
||||
{t("dragFiles")}
|
||||
</span>
|
||||
<span className="text-muted-foreground">{t("or")}</span>
|
||||
<div className="flex items-center gap-2 text-primary">
|
||||
<HiOutlineUpload size={24} />
|
||||
<span className="font-medium">{t("upload")}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">{filesOther}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-end gap-3">
|
||||
<button
|
||||
className="bg-black text-white px-4 py-2 rounded-md text-xl font-light"
|
||||
onClick={handleGoBack}
|
||||
>
|
||||
{t("back")}
|
||||
</button>
|
||||
<button
|
||||
className="bg-primary text-white px-4 py-2 rounded-md text-xl font-light"
|
||||
onClick={handleImageSubmit}
|
||||
>
|
||||
{t("next")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditComponentThree;
|
||||
322
components/PagesComponent/EditListing/EditComponentTwo.jsx
Normal file
322
components/PagesComponent/EditListing/EditComponentTwo.jsx
Normal file
@@ -0,0 +1,322 @@
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||
import { toast } from "sonner";
|
||||
import { HiOutlineUpload } from "react-icons/hi";
|
||||
import { MdOutlineAttachFile } from "react-icons/md";
|
||||
import CustomLink from "@/components/Common/CustomLink";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { handleKeyDown, inpNum, t } from "@/utils";
|
||||
import CustomImage from "@/components/Common/CustomImage";
|
||||
|
||||
const EditComponentTwo = ({
|
||||
customFields,
|
||||
setExtraDetails,
|
||||
handleGoBack,
|
||||
filePreviews,
|
||||
setFilePreviews,
|
||||
submitExtraDetails,
|
||||
currentExtraDetails,
|
||||
langId,
|
||||
defaultLangId,
|
||||
}) => {
|
||||
const write = (fieldId, value) =>
|
||||
setExtraDetails((prev) => ({
|
||||
...prev,
|
||||
[langId]: {
|
||||
...prev[langId],
|
||||
[fieldId]: value,
|
||||
},
|
||||
}));
|
||||
|
||||
const handleFileChange = (id, file) => {
|
||||
if (file) {
|
||||
const allowedExtensions = /\.(jpg|jpeg|svg|png|pdf)$/i;
|
||||
if (!allowedExtensions.test(file.name)) {
|
||||
toast.error(t("notAllowedFile"));
|
||||
return;
|
||||
}
|
||||
const fileUrl = URL.createObjectURL(file);
|
||||
setFilePreviews((prevPreviews) => ({
|
||||
...prevPreviews,
|
||||
[id]: {
|
||||
url: fileUrl,
|
||||
isPdf: /\.pdf$/i.test(file.name),
|
||||
},
|
||||
}));
|
||||
write(id, file);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCheckboxChange = (id, value, checked) => {
|
||||
const list = currentExtraDetails[id] || [];
|
||||
const next = checked
|
||||
? list.includes(value)
|
||||
? list
|
||||
: [...list, value]
|
||||
: list.filter((v) => v !== value);
|
||||
write(id, next);
|
||||
};
|
||||
|
||||
const handleChange = (id, value) => write(id, value ?? "");
|
||||
|
||||
const renderCustomFields = (field) => {
|
||||
let {
|
||||
id,
|
||||
translated_name,
|
||||
name,
|
||||
type,
|
||||
translated_value,
|
||||
values,
|
||||
min_length,
|
||||
max_length,
|
||||
} = field;
|
||||
|
||||
const inputProps = {
|
||||
id,
|
||||
name: id,
|
||||
onChange: (e) => handleChange(id, e.target.value),
|
||||
value: currentExtraDetails[id] || "",
|
||||
...(type === "number"
|
||||
? { min: min_length, max: max_length }
|
||||
: { minLength: min_length, maxLength: max_length }),
|
||||
};
|
||||
|
||||
switch (type) {
|
||||
case "number":
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<Input
|
||||
type={type}
|
||||
inputMode="numeric"
|
||||
placeholder={`${t("enter")} ${translated_name || name}`}
|
||||
{...inputProps}
|
||||
onKeyDown={(e) => handleKeyDown(e, max_length)}
|
||||
onKeyPress={(e) => inpNum(e)}
|
||||
/>
|
||||
{max_length && (
|
||||
<span className="self-end text-sm text-muted-foreground">
|
||||
{`${currentExtraDetails[id]?.length ?? 0}/${max_length}`}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
case "textbox":
|
||||
return (
|
||||
<div className=" flex flex-col">
|
||||
<Textarea
|
||||
placeholder={`${t("enter")} ${translated_name || name}`}
|
||||
{...inputProps}
|
||||
/>
|
||||
{max_length && (
|
||||
<span className="self-end text-sm text-muted-foreground">
|
||||
{`${currentExtraDetails[id]?.length ?? 0}/${max_length}`}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
case "dropdown":
|
||||
return (
|
||||
<div className="w-full">
|
||||
<Select
|
||||
id={id}
|
||||
name={id}
|
||||
value={currentExtraDetails[id] || ""}
|
||||
onValueChange={(value) => handleChange(id, value)}
|
||||
>
|
||||
<SelectTrigger className="outline-none focus:outline-none">
|
||||
<SelectValue
|
||||
className="font-medium"
|
||||
placeholder={`${t("select")} ${translated_name || name}`}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectLabel value="">
|
||||
{t("select")} {translated_name || name}
|
||||
</SelectLabel>
|
||||
{values?.map((option, index) => (
|
||||
<SelectItem
|
||||
id={option}
|
||||
className="font-medium"
|
||||
key={option}
|
||||
value={option}
|
||||
>
|
||||
{translated_value[index] || option}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
);
|
||||
|
||||
case "checkbox":
|
||||
return (
|
||||
<div className="flex w-full flex-wrap gap-2">
|
||||
{values?.map((value, index) => {
|
||||
const uniqueId = `${id}-${value}-${index}`;
|
||||
return (
|
||||
<div key={uniqueId} className="flex gap-1 items-center">
|
||||
<Checkbox
|
||||
id={uniqueId}
|
||||
value={value}
|
||||
onCheckedChange={(checked) =>
|
||||
handleCheckboxChange(id, value, checked)
|
||||
}
|
||||
checked={currentExtraDetails[id]?.includes(value)}
|
||||
/>
|
||||
<label
|
||||
htmlFor={uniqueId}
|
||||
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||
>
|
||||
{translated_value[index] || value}
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
case "radio":
|
||||
return (
|
||||
<RadioGroup
|
||||
value={currentExtraDetails[id] || ""}
|
||||
onValueChange={(value) => handleChange(id, value)}
|
||||
className="flex gap-2 flex-wrap"
|
||||
>
|
||||
{values?.map((option, index) => {
|
||||
const uniqueId = `${id}-${option}-${index}`;
|
||||
return (
|
||||
<div
|
||||
key={uniqueId}
|
||||
className="flex items-center gap-2 flex-wrap"
|
||||
>
|
||||
<RadioGroupItem
|
||||
value={option}
|
||||
id={uniqueId}
|
||||
className="sr-only peer "
|
||||
/>
|
||||
<label
|
||||
htmlFor={uniqueId}
|
||||
className={`${currentExtraDetails[id] === option
|
||||
? "bg-primary text-white"
|
||||
: ""
|
||||
} border rounded-md px-4 py-2 cursor-pointer transition-colors`}
|
||||
>
|
||||
{translated_value[index] || option}
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</RadioGroup>
|
||||
);
|
||||
|
||||
case "fileinput":
|
||||
const fileUrl = filePreviews[id]?.url;
|
||||
return (
|
||||
<>
|
||||
<label htmlFor={id} className="flex gap-2 items-center">
|
||||
<div className="cursor-pointer border px-2.5 py-1 rounded">
|
||||
<HiOutlineUpload size={24} fontWeight="400" />
|
||||
</div>
|
||||
{filePreviews[id] && (
|
||||
<div className="flex items-center gap-1 text-sm flex-nowrap break-words">
|
||||
{filePreviews[id]?.isPdf ? (
|
||||
<>
|
||||
<MdOutlineAttachFile />
|
||||
<CustomLink
|
||||
href={fileUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{t("viewPdf")}
|
||||
</CustomLink>
|
||||
</>
|
||||
) : (
|
||||
<CustomImage
|
||||
key={fileUrl}
|
||||
src={fileUrl}
|
||||
alt="Preview"
|
||||
className="h-9 w-9"
|
||||
height={36}
|
||||
width={36}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</label>
|
||||
<input
|
||||
type="file"
|
||||
id={id}
|
||||
name={name}
|
||||
className="hidden"
|
||||
onChange={(e) => handleFileChange(id, e.target.files[0])}
|
||||
accept=".jpg,.jpeg,.png,.svg,.pdf"
|
||||
/>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{t("allowedFileType")}
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-8">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{customFields?.map((field) => {
|
||||
if (langId !== defaultLangId && field.type !== "textbox") return null;
|
||||
return (
|
||||
<div className="flex flex-col w-full gap-2" key={field?.id}>
|
||||
<div className="flex gap-2 items-center">
|
||||
<CustomImage
|
||||
src={field?.image}
|
||||
alt={field?.name}
|
||||
height={28}
|
||||
width={28}
|
||||
className="h-7 w-7 rounded-sm"
|
||||
/>
|
||||
<Label
|
||||
className={`${field?.required === 1 && defaultLangId === langId
|
||||
? "requiredInputLabel"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
{field?.translated_name || field?.name}
|
||||
</Label>
|
||||
</div>
|
||||
{renderCustomFields(field)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="flex justify-end gap-3">
|
||||
<button
|
||||
className="bg-black text-white px-4 py-2 rounded-md text-xl font-light"
|
||||
onClick={handleGoBack}
|
||||
>
|
||||
{t("back")}
|
||||
</button>
|
||||
<button
|
||||
className="bg-primary text-white px-4 py-2 rounded-md text-xl font-light"
|
||||
onClick={submitExtraDetails}
|
||||
>
|
||||
{t("next")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default EditComponentTwo;
|
||||
538
components/PagesComponent/EditListing/EditListing.jsx
Normal file
538
components/PagesComponent/EditListing/EditListing.jsx
Normal file
@@ -0,0 +1,538 @@
|
||||
"use client";
|
||||
import { useEffect, useState } from "react";
|
||||
import BreadCrumb from "@/components/BreadCrumb/BreadCrumb";
|
||||
import {
|
||||
editItemApi,
|
||||
getCurrenciesApi,
|
||||
getCustomFieldsApi,
|
||||
getMyItemsApi,
|
||||
getParentCategoriesApi,
|
||||
} from "@/utils/api";
|
||||
import {
|
||||
filterNonDefaultTranslations,
|
||||
getMainDetailsTranslations,
|
||||
isValidURL,
|
||||
prefillExtraDetails,
|
||||
prepareCustomFieldFiles,
|
||||
prepareCustomFieldTranslations,
|
||||
t,
|
||||
validateExtraDetails,
|
||||
} from "@/utils";
|
||||
import EditComponentOne from "./EditComponentOne";
|
||||
import EditComponentTwo from "./EditComponentTwo";
|
||||
import EditComponentThree from "./EditComponentThree";
|
||||
import EditComponentFour from "./EditComponentFour";
|
||||
import { toast } from "sonner";
|
||||
import Layout from "@/components/Layout/Layout";
|
||||
import Checkauth from "@/HOC/Checkauth";
|
||||
import { CurrentLanguageData } from "@/redux/reducer/languageSlice";
|
||||
import { useSelector } from "react-redux";
|
||||
import AdSuccessModal from "../AdsListing/AdSuccessModal";
|
||||
import {
|
||||
getDefaultLanguageCode,
|
||||
getLanguages,
|
||||
} from "@/redux/reducer/settingSlice";
|
||||
import AdLanguageSelector from "../AdsListing/AdLanguageSelector";
|
||||
import PageLoader from "@/components/Common/PageLoader";
|
||||
import { isValidPhoneNumber } from "libphonenumber-js/max";
|
||||
|
||||
const EditListing = ({ id }) => {
|
||||
const CurrentLanguage = useSelector(CurrentLanguageData);
|
||||
const [step, setStep] = useState(1);
|
||||
const [CreatedAdSlug, setCreatedAdSlug] = useState("");
|
||||
const [openSuccessModal, setOpenSuccessModal] = useState(false);
|
||||
const [selectedCategoryPath, setSelectedCategoryPath] = useState([]);
|
||||
const [customFields, setCustomFields] = useState([]);
|
||||
const [uploadedImages, setUploadedImages] = useState([]);
|
||||
const [OtherImages, setOtherImages] = useState([]);
|
||||
const [Location, setLocation] = useState({});
|
||||
const [currencies, setCurrencies] = useState([]);
|
||||
const [filePreviews, setFilePreviews] = useState({});
|
||||
const [deleteImagesId, setDeleteImagesId] = useState("");
|
||||
const [isAdPlaced, setIsAdPlaced] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const languages = useSelector(getLanguages);
|
||||
const defaultLanguageCode = useSelector(getDefaultLanguageCode);
|
||||
const defaultLangId = languages?.find(
|
||||
(lang) => lang.code === defaultLanguageCode
|
||||
)?.id;
|
||||
|
||||
const [extraDetails, setExtraDetails] = useState({
|
||||
[defaultLangId]: {},
|
||||
});
|
||||
const [langId, setLangId] = useState(defaultLangId);
|
||||
|
||||
const [translations, setTranslations] = useState({
|
||||
[defaultLangId]: {},
|
||||
});
|
||||
const hasTextbox = customFields.some((field) => field.type === "textbox");
|
||||
|
||||
const defaultDetails = translations[defaultLangId] || {};
|
||||
const currentDetails = translations[langId] || {};
|
||||
const currentExtraDetails = extraDetails[langId] || {};
|
||||
|
||||
const is_job_category =
|
||||
Number(
|
||||
selectedCategoryPath[selectedCategoryPath.length - 1]?.is_job_category
|
||||
) === 1;
|
||||
const isPriceOptional =
|
||||
Number(
|
||||
selectedCategoryPath[selectedCategoryPath.length - 1]?.price_optional
|
||||
) === 1;
|
||||
|
||||
useEffect(() => {
|
||||
getSingleListingData();
|
||||
}, [CurrentLanguage.id]);
|
||||
|
||||
const fetchCategoryPath = async (childCategoryId) => {
|
||||
try {
|
||||
const categoryResponse =
|
||||
await getParentCategoriesApi.getPaymentCategories({
|
||||
child_category_id: childCategoryId,
|
||||
});
|
||||
setSelectedCategoryPath(categoryResponse?.data?.data);
|
||||
} catch (error) {
|
||||
console.log("Error fetching category path:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const getCustomFields = async (categoryIds, extraFieldValue) => {
|
||||
try {
|
||||
const customFieldsRes = await getCustomFieldsApi.getCustomFields({
|
||||
category_ids: categoryIds,
|
||||
});
|
||||
const data = customFieldsRes?.data?.data;
|
||||
setCustomFields(data);
|
||||
const tempExtraDetails = prefillExtraDetails({
|
||||
data,
|
||||
languages,
|
||||
defaultLangId,
|
||||
extraFieldValue,
|
||||
setFilePreviews,
|
||||
});
|
||||
setExtraDetails(tempExtraDetails);
|
||||
setLangId(defaultLangId);
|
||||
} catch (error) {
|
||||
console.log("Error fetching custom fields:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const getCurrencies = async () => {
|
||||
try {
|
||||
const res = await getCurrenciesApi.getCurrencies();
|
||||
const currenciesData = res?.data?.data || [];
|
||||
setCurrencies(currenciesData);
|
||||
return currenciesData; // Return the currencies data
|
||||
} catch (error) {
|
||||
console.log("error", error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
const getSingleListingData = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const res = await getMyItemsApi.getMyItems({ id: Number(id) });
|
||||
const listingData = res?.data?.data?.data?.[0];
|
||||
|
||||
if (!listingData) {
|
||||
throw new Error("Listing not found");
|
||||
}
|
||||
// Get currencies data directly
|
||||
const [_, __, currenciesData] = await Promise.all([
|
||||
getCustomFields(
|
||||
listingData.all_category_ids,
|
||||
listingData?.all_translated_custom_fields
|
||||
),
|
||||
fetchCategoryPath(listingData?.category_id),
|
||||
getCurrencies(),
|
||||
]);
|
||||
|
||||
setUploadedImages(listingData?.image);
|
||||
setOtherImages(listingData?.gallery_images);
|
||||
|
||||
const mainDetailsTranslation = getMainDetailsTranslations(
|
||||
listingData,
|
||||
languages,
|
||||
defaultLangId,
|
||||
currenciesData
|
||||
);
|
||||
setTranslations(mainDetailsTranslation);
|
||||
setLocation({
|
||||
country: listingData?.country,
|
||||
state: listingData?.state,
|
||||
city: listingData?.city,
|
||||
formattedAddress: listingData?.translated_address,
|
||||
lat: listingData?.latitude,
|
||||
long: listingData?.longitude,
|
||||
area_id: listingData?.area_id ? listingData?.area_id : null,
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDetailsSubmit = () => {
|
||||
if (customFields?.length === 0) {
|
||||
setStep(3);
|
||||
return;
|
||||
}
|
||||
setStep(2);
|
||||
};
|
||||
|
||||
const handleImageSubmit = () => {
|
||||
if (uploadedImages.length === 0) {
|
||||
toast.error(t("uploadMainPicture"));
|
||||
return;
|
||||
}
|
||||
setStep(4);
|
||||
};
|
||||
|
||||
const handleGoBack = () => {
|
||||
if (step == 3 && customFields?.length == 0) {
|
||||
setStep((prev) => prev - 2);
|
||||
} else {
|
||||
setStep((prev) => prev - 1);
|
||||
}
|
||||
};
|
||||
|
||||
const handleTabClick = (tab) => {
|
||||
if (tab === 1) {
|
||||
setStep(1);
|
||||
} else if (tab === 2) {
|
||||
setStep(2);
|
||||
} else if (tab === 3) {
|
||||
setStep(3);
|
||||
} else if (tab === 4) {
|
||||
setStep(4);
|
||||
}
|
||||
};
|
||||
|
||||
const submitExtraDetails = () => {
|
||||
setStep(3);
|
||||
};
|
||||
|
||||
const SLUG_RE = /^[a-z0-9-]+$/i;
|
||||
const isEmpty = (x) => !x || !x.toString().trim();
|
||||
const isNegative = (n) => Number(n) < 0;
|
||||
|
||||
const handleFullSubmission = () => {
|
||||
const {
|
||||
name,
|
||||
description,
|
||||
price,
|
||||
slug,
|
||||
contact,
|
||||
video_link,
|
||||
min_salary,
|
||||
max_salary,
|
||||
country_code,
|
||||
} = defaultDetails;
|
||||
|
||||
if (!name.trim() || !description.trim()) {
|
||||
toast.error(t("completeDetails"));
|
||||
setStep(1);
|
||||
return;
|
||||
}
|
||||
|
||||
// ✅ Validate phone number ONLY if user entered one as it is optional
|
||||
if (Boolean(contact) && !isValidPhoneNumber(`+${country_code}${contact}`)) {
|
||||
toast.error(t("invalidPhoneNumber"));
|
||||
return setStep(1);
|
||||
}
|
||||
|
||||
if (is_job_category) {
|
||||
const min = min_salary ? Number(min_salary) : null;
|
||||
const max = max_salary ? Number(max_salary) : null;
|
||||
|
||||
// Salary fields are optional, but validate if provided
|
||||
if (min !== null && min < 0) {
|
||||
toast.error(t("enterValidSalaryMin"));
|
||||
setStep(1);
|
||||
return;
|
||||
}
|
||||
|
||||
if (max !== null && max < 0) {
|
||||
toast.error(t("enterValidSalaryMax"));
|
||||
setStep(1);
|
||||
return;
|
||||
}
|
||||
if (min !== null && max !== null) {
|
||||
if (min === max) {
|
||||
toast.error(t("salaryMinCannotBeEqualMax"));
|
||||
return setStep(1);
|
||||
}
|
||||
if (min > max) {
|
||||
toast.error(t("salaryMinCannotBeGreaterThanMax"));
|
||||
return setStep(1);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (!isPriceOptional && isEmpty(price)) {
|
||||
toast.error(t("completeDetails"));
|
||||
return setStep(1);
|
||||
}
|
||||
|
||||
if (!isEmpty(price) && isNegative(price)) {
|
||||
toast.error(t("enterValidPrice"));
|
||||
return setStep(1);
|
||||
}
|
||||
}
|
||||
|
||||
if (!isEmpty(slug) && !SLUG_RE.test(slug.trim())) {
|
||||
toast.error(t("addValidSlug"));
|
||||
return setStep(1);
|
||||
}
|
||||
|
||||
if (!isEmpty(video_link) && !isValidURL(video_link)) {
|
||||
toast.error(t("enterValidUrl"));
|
||||
setStep(1);
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
customFields.length !== 0 &&
|
||||
!validateExtraDetails({
|
||||
languages,
|
||||
defaultLangId,
|
||||
extraDetails,
|
||||
customFields,
|
||||
filePreviews,
|
||||
})
|
||||
) {
|
||||
setStep(2);
|
||||
return;
|
||||
}
|
||||
|
||||
if (uploadedImages.length === 0) {
|
||||
toast.error(t("uploadMainPicture"));
|
||||
setStep(3);
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
!Location?.country ||
|
||||
!Location?.state ||
|
||||
!Location?.city ||
|
||||
!Location?.formattedAddress
|
||||
) {
|
||||
toast.error(t("pleaseSelectCity"));
|
||||
return;
|
||||
}
|
||||
editAd();
|
||||
};
|
||||
|
||||
const editAd = async () => {
|
||||
const nonDefaultTranslations = filterNonDefaultTranslations(
|
||||
translations,
|
||||
defaultLangId
|
||||
);
|
||||
const customFieldTranslations =
|
||||
prepareCustomFieldTranslations(extraDetails);
|
||||
|
||||
const customFieldFiles = prepareCustomFieldFiles(
|
||||
extraDetails,
|
||||
defaultLangId
|
||||
);
|
||||
|
||||
const allData = {
|
||||
id: id,
|
||||
name: defaultDetails.name,
|
||||
slug: defaultDetails.slug.trim(),
|
||||
description: defaultDetails?.description,
|
||||
price: defaultDetails.price,
|
||||
contact: defaultDetails.contact,
|
||||
region_code: defaultDetails?.region_code?.toUpperCase() || "",
|
||||
video_link: defaultDetails?.video_link,
|
||||
// custom_fields: transformedCustomFields,
|
||||
image: typeof uploadedImages == "string" ? null : uploadedImages[0],
|
||||
gallery_images: OtherImages,
|
||||
address: Location?.formattedAddress,
|
||||
latitude: Location?.lat,
|
||||
longitude: Location?.long,
|
||||
custom_field_files: customFieldFiles,
|
||||
country: Location?.country,
|
||||
state: Location?.state,
|
||||
city: Location?.city,
|
||||
...(Location?.area_id ? { area_id: Number(Location?.area_id) } : {}),
|
||||
delete_item_image_id: deleteImagesId,
|
||||
...(Object.keys(nonDefaultTranslations).length > 0 && {
|
||||
translations: nonDefaultTranslations,
|
||||
}),
|
||||
...(defaultDetails?.currency_id && {
|
||||
currency_id: defaultDetails?.currency_id,
|
||||
}),
|
||||
...(Object.keys(customFieldTranslations).length > 0 && {
|
||||
custom_field_translations: customFieldTranslations,
|
||||
}),
|
||||
// expiry_date: '2025-10-13'
|
||||
};
|
||||
|
||||
if (is_job_category) {
|
||||
// Only add salary fields if they're provided
|
||||
allData.min_salary = defaultDetails.min_salary;
|
||||
allData.max_salary = defaultDetails.max_salary;
|
||||
} else {
|
||||
allData.price = defaultDetails.price;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsAdPlaced(true);
|
||||
const res = await editItemApi.editItem(allData);
|
||||
if (res?.data?.error === false) {
|
||||
setOpenSuccessModal(true);
|
||||
setCreatedAdSlug(res?.data?.data[0]?.slug);
|
||||
} else {
|
||||
toast.error(res?.data?.message);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
} finally {
|
||||
setIsAdPlaced(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
{isLoading ? (
|
||||
<PageLoader />
|
||||
) : (
|
||||
<>
|
||||
<BreadCrumb title2={t("editListing")} />
|
||||
<div className="container">
|
||||
<div className="flex flex-col gap-6 mt-8">
|
||||
<h1 className="text-2xl font-medium">{t("editListing")}</h1>
|
||||
<div className="flex flex-col gap-6 border rounded-md p-4">
|
||||
<div className="flex items-center gap-3 justify-between bg-muted px-4 py-2 rounded-md flex-wrap">
|
||||
<div className="flex items-center gap-3 flex-wrap">
|
||||
<div
|
||||
className={`transition-all duration-300 p-2 cursor-pointer ${step === 1 ? "bg-primary text-white" : ""
|
||||
} rounded-md `}
|
||||
onClick={() => handleTabClick(1)}
|
||||
>
|
||||
{t("details")}
|
||||
</div>
|
||||
{customFields?.length > 0 && (
|
||||
<div
|
||||
className={`transition-all duration-300 p-2 cursor-pointer ${step === 2 ? "bg-primary text-white" : ""
|
||||
} rounded-md`}
|
||||
onClick={() => handleTabClick(2)}
|
||||
>
|
||||
{t("extraDetails")}
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={`transition-all duration-300 p-2 cursor-pointer ${step === 3 ? "bg-primary text-white" : ""
|
||||
} rounded-md `}
|
||||
onClick={() => handleTabClick(3)}
|
||||
>
|
||||
{t("images")}
|
||||
</div>
|
||||
<div
|
||||
className={`transition-all duration-300 p-2 cursor-pointer ${step === 4 ? "bg-primary text-white" : ""
|
||||
} rounded-md `}
|
||||
onClick={() => handleTabClick(4)}
|
||||
>
|
||||
{t("location")}
|
||||
</div>
|
||||
</div>
|
||||
{(step === 1 || (step === 2 && hasTextbox)) && (
|
||||
<AdLanguageSelector
|
||||
langId={langId}
|
||||
setLangId={setLangId}
|
||||
languages={languages}
|
||||
setTranslations={setTranslations}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{step === 1 &&
|
||||
selectedCategoryPath &&
|
||||
selectedCategoryPath?.length > 0 && (
|
||||
<div className="flex flex-col gap-2">
|
||||
<h1 className="font-medium text-xl">
|
||||
{t("selectedCategory")}
|
||||
</h1>
|
||||
<div className="flex">
|
||||
{selectedCategoryPath?.map((item, index) => {
|
||||
const shouldShowComma =
|
||||
selectedCategoryPath.length > 1 &&
|
||||
index !== selectedCategoryPath.length - 1;
|
||||
return (
|
||||
<span className="text-primary" key={item.id}>
|
||||
{item.name}
|
||||
{shouldShowComma && ", "}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
{step == 1 && (
|
||||
<EditComponentOne
|
||||
setTranslations={setTranslations}
|
||||
current={currentDetails}
|
||||
langId={langId}
|
||||
defaultLangId={defaultLangId}
|
||||
handleDetailsSubmit={handleDetailsSubmit}
|
||||
is_job_category={is_job_category}
|
||||
isPriceOptional={isPriceOptional}
|
||||
currencies={currencies}
|
||||
/>
|
||||
)}
|
||||
|
||||
{step == 2 && customFields.length > 0 && (
|
||||
<EditComponentTwo
|
||||
customFields={customFields}
|
||||
extraDetails={extraDetails}
|
||||
setExtraDetails={setExtraDetails}
|
||||
handleGoBack={handleGoBack}
|
||||
filePreviews={filePreviews}
|
||||
setFilePreviews={setFilePreviews}
|
||||
submitExtraDetails={submitExtraDetails}
|
||||
currentExtraDetails={currentExtraDetails}
|
||||
langId={langId}
|
||||
defaultLangId={defaultLangId}
|
||||
/>
|
||||
)}
|
||||
|
||||
{step == 3 && (
|
||||
<EditComponentThree
|
||||
setUploadedImages={setUploadedImages}
|
||||
uploadedImages={uploadedImages}
|
||||
OtherImages={OtherImages}
|
||||
setOtherImages={setOtherImages}
|
||||
handleImageSubmit={handleImageSubmit}
|
||||
handleGoBack={handleGoBack}
|
||||
setDeleteImagesId={setDeleteImagesId}
|
||||
/>
|
||||
)}
|
||||
|
||||
{step == 4 && (
|
||||
<EditComponentFour
|
||||
handleGoBack={handleGoBack}
|
||||
location={Location}
|
||||
setLocation={setLocation}
|
||||
handleFullSubmission={handleFullSubmission}
|
||||
isAdPlaced={isAdPlaced}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<AdSuccessModal
|
||||
openSuccessModal={openSuccessModal}
|
||||
setOpenSuccessModal={setOpenSuccessModal}
|
||||
createdAdSlug={CreatedAdSlug}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export default Checkauth(EditListing);
|
||||
Reference in New Issue
Block a user