classify web
This commit is contained in:
54
components/PagesComponent/AdsListing/AdLanguageSelector.jsx
Normal file
54
components/PagesComponent/AdsListing/AdLanguageSelector.jsx
Normal file
@@ -0,0 +1,54 @@
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { getIsRtl } from "@/redux/reducer/languageSlice";
|
||||
import { t } from "@/utils";
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
const AdLanguageSelector = ({
|
||||
langId,
|
||||
setLangId,
|
||||
languages,
|
||||
setTranslations,
|
||||
}) => {
|
||||
const isRTL = useSelector(getIsRtl);
|
||||
|
||||
const handleLangChange = (newId) => {
|
||||
setLangId(newId);
|
||||
setTranslations((t) => ({
|
||||
...t,
|
||||
[newId]: t[newId] || {},
|
||||
}));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<p className="whitespace-nowrap text-sm font-medium hidden lg:block">
|
||||
{t("selectLanguage")}
|
||||
</p>
|
||||
<Select value={langId} onValueChange={handleLangChange}>
|
||||
<SelectTrigger className="gap-2">
|
||||
<SelectValue placeholder="Select language" />
|
||||
</SelectTrigger>
|
||||
<SelectContent align={isRTL ? "start" : "end"}>
|
||||
<SelectGroup>
|
||||
{languages &&
|
||||
languages.length > 0 &&
|
||||
languages.map((lang) => (
|
||||
<SelectItem key={lang.id} value={lang.id}>
|
||||
{lang.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdLanguageSelector;
|
||||
55
components/PagesComponent/AdsListing/AdSuccessModal.jsx
Normal file
55
components/PagesComponent/AdsListing/AdSuccessModal.jsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} 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 AdSuccessModal = ({
|
||||
openSuccessModal,
|
||||
setOpenSuccessModal,
|
||||
createdAdSlug,
|
||||
}) => {
|
||||
const closeSuccessModal = () => {
|
||||
setOpenSuccessModal(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={openSuccessModal} onOpenChange={closeSuccessModal}>
|
||||
<DialogContent
|
||||
className="[&>button]:hidden !max-w-[520px] py-[50px] px-[30px] sm:px-[80px]"
|
||||
onInteractOutside={(e) => {
|
||||
e.preventDefault();
|
||||
}}
|
||||
>
|
||||
<DialogHeader className="flex flex-col gap-4 items-center">
|
||||
<CustomImage
|
||||
src={trueGif}
|
||||
alt="success"
|
||||
height={176}
|
||||
width={176}
|
||||
className="h-44 w-44"
|
||||
/>
|
||||
<DialogTitle className="text-3xl font-semibold text-center !p-0 mt-0">
|
||||
{t("adPostedSuccess")}
|
||||
</DialogTitle>
|
||||
<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>
|
||||
</DialogHeader>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdSuccessModal;
|
||||
753
components/PagesComponent/AdsListing/AdsListing.jsx
Normal file
753
components/PagesComponent/AdsListing/AdsListing.jsx
Normal file
@@ -0,0 +1,753 @@
|
||||
"use client";
|
||||
import { useEffect, useState } from "react";
|
||||
import ComponentOne from "./ComponentOne";
|
||||
import {
|
||||
addItemApi,
|
||||
categoryApi,
|
||||
getCurrenciesApi,
|
||||
getCustomFieldsApi,
|
||||
getParentCategoriesApi,
|
||||
} from "@/utils/api";
|
||||
import ComponentTwo from "./ComponentTwo";
|
||||
import {
|
||||
filterNonDefaultTranslations,
|
||||
getDefaultCountryCode,
|
||||
isValidURL,
|
||||
prepareCustomFieldFiles,
|
||||
prepareCustomFieldTranslations,
|
||||
t,
|
||||
validateExtraDetails,
|
||||
} from "@/utils";
|
||||
import { toast } from "sonner";
|
||||
import ComponentThree from "./ComponentThree";
|
||||
import ComponentFour from "./ComponentFour";
|
||||
import ComponentFive from "./ComponentFive";
|
||||
import { useSelector } from "react-redux";
|
||||
import AdSuccessModal from "./AdSuccessModal";
|
||||
import BreadCrumb from "@/components/BreadCrumb/BreadCrumb";
|
||||
import Layout from "@/components/Layout/Layout";
|
||||
import Checkauth from "@/HOC/Checkauth";
|
||||
import { CurrentLanguageData } from "@/redux/reducer/languageSlice";
|
||||
import AdLanguageSelector from "./AdLanguageSelector";
|
||||
import {
|
||||
getDefaultLanguageCode,
|
||||
getLanguages,
|
||||
} from "@/redux/reducer/settingSlice";
|
||||
import { userSignUpData } from "@/redux/reducer/authSlice";
|
||||
import { isValidPhoneNumber } from "libphonenumber-js/max";
|
||||
import { getCurrentCountry } from "@/redux/reducer/locationSlice";
|
||||
|
||||
const AdsListing = () => {
|
||||
const CurrentLanguage = useSelector(CurrentLanguageData);
|
||||
const currentCountry = useSelector(getCurrentCountry);
|
||||
const [step, setStep] = useState(1);
|
||||
const [categories, setCategories] = useState();
|
||||
const [categoriesLoading, setCategoriesLoading] = useState(false);
|
||||
const [isLoadMoreCat, setIsLoadMoreCat] = useState(false);
|
||||
const [categoryPath, setCategoryPath] = useState([]);
|
||||
const [currentPage, setCurrentPage] = useState();
|
||||
const [lastPage, setLastPage] = useState();
|
||||
const [disabledTab, setDisabledTab] = useState({
|
||||
categoryTab: false,
|
||||
detailTab: true,
|
||||
extraDetailTabl: true,
|
||||
images: true,
|
||||
location: true,
|
||||
});
|
||||
const [customFields, setCustomFields] = useState([]);
|
||||
const [filePreviews, setFilePreviews] = useState({});
|
||||
const [uploadedImages, setUploadedImages] = useState([]);
|
||||
const [otherImages, setOtherImages] = useState([]);
|
||||
const [location, setLocation] = useState({});
|
||||
const [currencies, setCurrencies] = useState([]);
|
||||
const [isAdPlaced, setIsAdPlaced] = useState(false);
|
||||
const [openSuccessModal, setOpenSuccessModal] = useState(false);
|
||||
const [createdAdSlug, setCreatedAdSlug] = useState("");
|
||||
const userData = useSelector(userSignUpData);
|
||||
|
||||
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 countryCode =
|
||||
userData?.country_code?.replace("+", "") || getDefaultCountryCode();
|
||||
const mobile = userData?.mobile || "";
|
||||
const regionCode =
|
||||
userData?.region_code?.toLowerCase() ||
|
||||
process.env.NEXT_PUBLIC_DEFAULT_COUNTRY?.toLowerCase() ||
|
||||
"in";
|
||||
|
||||
const [translations, setTranslations] = useState({
|
||||
[langId]: {
|
||||
contact: mobile,
|
||||
country_code: countryCode,
|
||||
region_code: regionCode,
|
||||
},
|
||||
});
|
||||
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(categoryPath[categoryPath.length - 1]?.is_job_category) === 1;
|
||||
const isPriceOptional =
|
||||
Number(categoryPath[categoryPath.length - 1]?.price_optional) === 1;
|
||||
|
||||
const allCategoryIdsString = categoryPath
|
||||
.map((category) => category.id)
|
||||
.join(",");
|
||||
let lastItemId = categoryPath[categoryPath.length - 1]?.id;
|
||||
|
||||
useEffect(() => {
|
||||
if (step === 1) {
|
||||
handleFetchCategories();
|
||||
}
|
||||
}, [CurrentLanguage.id]);
|
||||
|
||||
useEffect(() => {
|
||||
if (step !== 1 && allCategoryIdsString) {
|
||||
getCustomFieldsData();
|
||||
}
|
||||
}, [allCategoryIdsString, CurrentLanguage.id]);
|
||||
|
||||
useEffect(() => {
|
||||
// Update category path translations when language changes
|
||||
if (categoryPath.length > 0) {
|
||||
const lastCategoryId = categoryPath[categoryPath.length - 1]?.id;
|
||||
if (lastCategoryId) {
|
||||
getParentCategoriesApi
|
||||
.getPaymentCategories({
|
||||
child_category_id: lastCategoryId,
|
||||
})
|
||||
.then((res) => {
|
||||
const updatedPath = res?.data?.data;
|
||||
if (updatedPath?.length > 0) {
|
||||
setCategoryPath(updatedPath);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log("Error updating category path:", err);
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [CurrentLanguage.id]);
|
||||
|
||||
useEffect(() => {
|
||||
getCurrencies();
|
||||
}, [countryCode]);
|
||||
|
||||
const getCurrencies = async () => {
|
||||
try {
|
||||
let params = {};
|
||||
|
||||
if (currentCountry) {
|
||||
params.country = currentCountry;
|
||||
}
|
||||
const res = await getCurrenciesApi.getCurrencies(params);
|
||||
const currenciesData = res?.data?.data || [];
|
||||
setCurrencies(currenciesData);
|
||||
// 🚫 IMPORTANT: If no currencies, REMOVE currency_id
|
||||
if (currenciesData.length === 0) {
|
||||
setTranslations((prev) => {
|
||||
const updated = { ...prev };
|
||||
|
||||
if (updated[langId]?.currency_id) {
|
||||
delete updated[langId].currency_id;
|
||||
}
|
||||
|
||||
return updated;
|
||||
});
|
||||
return;
|
||||
}
|
||||
// ✅ Normal case: set default currency
|
||||
const defaultCurrency =
|
||||
currenciesData.find((c) => c.selected == 1) || currenciesData[0];
|
||||
|
||||
if (defaultCurrency && !translations[langId]?.currency_id) {
|
||||
setTranslations((prev) => ({
|
||||
...prev,
|
||||
[langId]: {
|
||||
...prev[langId],
|
||||
currency_id: defaultCurrency.id,
|
||||
},
|
||||
}));
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("error", error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFetchCategories = async (
|
||||
category,
|
||||
isWaitForApiResToUpdatePath = false
|
||||
) => {
|
||||
setCategoriesLoading(true);
|
||||
try {
|
||||
const categoryId = category ? category?.id : null;
|
||||
|
||||
const res = await categoryApi.getCategory({
|
||||
category_id: categoryId,
|
||||
listing: 1,
|
||||
});
|
||||
if (res?.data?.error === false) {
|
||||
const data = res?.data?.data?.data;
|
||||
setCategories(data);
|
||||
setCurrentPage(res?.data?.data?.current_page);
|
||||
setLastPage(res?.data?.data?.last_page);
|
||||
if (isWaitForApiResToUpdatePath) {
|
||||
setCategoryPath((prevPath) => [...prevPath, category]);
|
||||
if (category.subcategories_count == 0) {
|
||||
setStep(2);
|
||||
setDisabledTab({
|
||||
categoryTab: true,
|
||||
detailTab: false,
|
||||
extraDetailTabl: false,
|
||||
images: false,
|
||||
location: false,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const index = categoryPath.findIndex(
|
||||
(item) => item.id === category?.id
|
||||
);
|
||||
setCategoryPath((prevPath) => prevPath.slice(0, index + 1));
|
||||
}
|
||||
} else {
|
||||
toast.error(res?.data?.message);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("error", error);
|
||||
} finally {
|
||||
setCategoriesLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getCustomFieldsData = async () => {
|
||||
try {
|
||||
const res = await getCustomFieldsApi.getCustomFields({
|
||||
category_ids: allCategoryIdsString,
|
||||
});
|
||||
const data = res?.data?.data;
|
||||
setCustomFields(data);
|
||||
|
||||
const initializedDetails = {};
|
||||
|
||||
languages.forEach((lang) => {
|
||||
const langFields = {};
|
||||
|
||||
data.forEach((item) => {
|
||||
if (lang.id !== defaultLangId && item.type !== "textbox") return;
|
||||
|
||||
let initialValue = "";
|
||||
switch (item.type) {
|
||||
case "checkbox":
|
||||
case "radio":
|
||||
initialValue = [];
|
||||
break;
|
||||
case "fileinput":
|
||||
initialValue = null;
|
||||
break;
|
||||
case "dropdown":
|
||||
case "textbox":
|
||||
case "number":
|
||||
case "text":
|
||||
initialValue = "";
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
langFields[item.id] = initialValue;
|
||||
});
|
||||
initializedDetails[lang.id] = langFields;
|
||||
});
|
||||
setExtraDetails(initializedDetails);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCategoryTabClick = async (category) => {
|
||||
await handleFetchCategories(category, true);
|
||||
};
|
||||
|
||||
const handleSelectedTabClick = (id) => {
|
||||
setCustomFields([]);
|
||||
setLangId(defaultLangId);
|
||||
setTranslations({
|
||||
[defaultLangId]: {
|
||||
contact: mobile,
|
||||
country_code: countryCode,
|
||||
region_code: regionCode,
|
||||
},
|
||||
});
|
||||
setExtraDetails({
|
||||
[defaultLangId]: {},
|
||||
});
|
||||
if (step !== 1) {
|
||||
setStep(1);
|
||||
setDisabledTab({
|
||||
categoryTab: false,
|
||||
detailTab: true,
|
||||
extraDetailTabl: true,
|
||||
images: true,
|
||||
location: true,
|
||||
});
|
||||
}
|
||||
// ✅ SINGLE CATEGORY EDGE CASE
|
||||
|
||||
const index = categoryPath.findIndex((item) => item.id === id);
|
||||
|
||||
if (index === 0) {
|
||||
setCategoryPath([]);
|
||||
// Fetch root / all categories
|
||||
handleFetchCategories(null);
|
||||
return;
|
||||
}
|
||||
|
||||
// ✅ NORMAL BACK-NAVIGATION FLOW
|
||||
const secondLast = categoryPath[index - 1];
|
||||
|
||||
if (secondLast) {
|
||||
handleFetchCategories(secondLast);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDetailsSubmit = () => {
|
||||
if (customFields?.length === 0) {
|
||||
setStep(4);
|
||||
} else {
|
||||
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;
|
||||
|
||||
// Step 1: Must pick a category
|
||||
const catId = categoryPath.at(-1)?.id;
|
||||
|
||||
if (!catId) {
|
||||
toast.error(t("selectCategory"));
|
||||
return setStep(1);
|
||||
}
|
||||
// Step 2: Get data for default (selected) language
|
||||
if (isEmpty(name) || isEmpty(description)) {
|
||||
toast.error(t("completeDetails")); // Title, desc, phone required
|
||||
return setStep(2);
|
||||
}
|
||||
|
||||
// ✅ 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(2);
|
||||
}
|
||||
|
||||
// Step 3: Validate job or price fields
|
||||
if (is_job_category) {
|
||||
const min = min_salary ? Number(min_salary) : null;
|
||||
const max = max_salary ? Number(max_salary) : null;
|
||||
|
||||
if (min !== null && min < 0) {
|
||||
toast.error(t("enterValidSalaryMin"));
|
||||
return setStep(2);
|
||||
}
|
||||
if (max !== null && max < 0) {
|
||||
toast.error(t("enterValidSalaryMax"));
|
||||
return setStep(2);
|
||||
}
|
||||
if (min !== null && max !== null) {
|
||||
if (min === max) {
|
||||
toast.error(t("salaryMinCannotBeEqualMax"));
|
||||
return setStep(2);
|
||||
}
|
||||
if (min > max) {
|
||||
toast.error(t("salaryMinCannotBeGreaterThanMax"));
|
||||
return setStep(2);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (!isPriceOptional && isEmpty(price)) {
|
||||
toast.error(t("completeDetails")); // Price is required
|
||||
return setStep(2);
|
||||
}
|
||||
if (!isEmpty(price) && isNegative(price)) {
|
||||
toast.error(t("enterValidPrice"));
|
||||
return setStep(2);
|
||||
}
|
||||
}
|
||||
|
||||
// Step 4: Video URL check
|
||||
if (!isEmpty(video_link) && !isValidURL(video_link)) {
|
||||
toast.error(t("enterValidUrl"));
|
||||
return setStep(2);
|
||||
}
|
||||
|
||||
// Step 5: Slug validation
|
||||
if (!isEmpty(slug) && !SLUG_RE.test(slug.trim())) {
|
||||
toast.error(t("addValidSlug"));
|
||||
return setStep(2);
|
||||
}
|
||||
|
||||
if (
|
||||
customFields.length !== 0 &&
|
||||
!validateExtraDetails({
|
||||
languages,
|
||||
defaultLangId,
|
||||
extraDetails,
|
||||
customFields,
|
||||
filePreviews,
|
||||
})
|
||||
) {
|
||||
return setStep(3);
|
||||
}
|
||||
if (uploadedImages.length === 0) {
|
||||
toast.error(t("uploadMainPicture"));
|
||||
setStep(4);
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
!location?.country ||
|
||||
!location?.state ||
|
||||
!location?.city ||
|
||||
!location?.formattedAddress
|
||||
) {
|
||||
toast.error(t("pleaseSelectCity"));
|
||||
return;
|
||||
}
|
||||
postAd();
|
||||
};
|
||||
|
||||
const postAd = async () => {
|
||||
const catId = categoryPath.at(-1)?.id;
|
||||
const customFieldTranslations =
|
||||
prepareCustomFieldTranslations(extraDetails);
|
||||
|
||||
const customFieldFiles = prepareCustomFieldFiles(
|
||||
extraDetails,
|
||||
defaultLangId
|
||||
);
|
||||
const nonDefaultTranslations = filterNonDefaultTranslations(
|
||||
translations,
|
||||
defaultLangId
|
||||
);
|
||||
|
||||
const allData = {
|
||||
name: defaultDetails.name,
|
||||
slug: defaultDetails.slug.trim(),
|
||||
description: defaultDetails?.description,
|
||||
category_id: catId,
|
||||
all_category_ids: allCategoryIdsString,
|
||||
price: defaultDetails.price,
|
||||
contact: defaultDetails.contact,
|
||||
video_link: defaultDetails?.video_link,
|
||||
// custom_fields: transformedCustomFields,
|
||||
image: 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) } : {}),
|
||||
...(Object.keys(nonDefaultTranslations).length > 0 && {
|
||||
translations: nonDefaultTranslations,
|
||||
}),
|
||||
...(Object.keys(customFieldTranslations).length > 0 && {
|
||||
custom_field_translations: customFieldTranslations,
|
||||
}),
|
||||
...(defaultDetails?.currency_id && {
|
||||
currency_id: defaultDetails?.currency_id,
|
||||
}),
|
||||
region_code: defaultDetails?.region_code?.toUpperCase() || "",
|
||||
};
|
||||
if (is_job_category) {
|
||||
// Only add salary fields if they're provided
|
||||
if (defaultDetails.min_salary) {
|
||||
allData.min_salary = defaultDetails.min_salary;
|
||||
}
|
||||
if (defaultDetails.max_salary) {
|
||||
allData.max_salary = defaultDetails.max_salary;
|
||||
}
|
||||
} else {
|
||||
allData.price = defaultDetails.price;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsAdPlaced(true);
|
||||
const res = await addItemApi.addItem(allData);
|
||||
if (res?.data?.error === false) {
|
||||
setOpenSuccessModal(true);
|
||||
setCreatedAdSlug(res?.data?.data[0]?.slug);
|
||||
} else {
|
||||
toast.error(res?.data?.message);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
setIsAdPlaced(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleGoBack = () => {
|
||||
setStep((prev) => {
|
||||
if (customFields.length === 0 && step === 4) {
|
||||
return prev - 2;
|
||||
} else {
|
||||
return prev - 1;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const fetchMoreCategory = async () => {
|
||||
setIsLoadMoreCat(true);
|
||||
try {
|
||||
const response = await categoryApi.getCategory({
|
||||
page: `${currentPage + 1}`,
|
||||
category_id: lastItemId,
|
||||
listing: 1,
|
||||
});
|
||||
const { data } = response.data;
|
||||
setCategories((prev) => [...prev, ...data.data]);
|
||||
setCurrentPage(data?.current_page); // Update the current page
|
||||
setLastPage(data?.last_page); // Update the current page
|
||||
} catch (error) {
|
||||
console.error("Error:", error);
|
||||
} finally {
|
||||
setIsLoadMoreCat(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleTabClick = (tab) => {
|
||||
if (tab === 1 && !disabledTab.categoryTab) {
|
||||
setStep(1);
|
||||
} else if (tab === 2 && !disabledTab.detailTab) {
|
||||
setStep(2);
|
||||
} else if (tab === 3 && !disabledTab.extraDetailTabl) {
|
||||
setStep(3);
|
||||
} else if (tab === 4 && !disabledTab.images) {
|
||||
setStep(4);
|
||||
} else if (tab === 5 && !disabledTab.location) {
|
||||
setStep(5);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeatilsBack = () => {
|
||||
setCustomFields([]);
|
||||
setLangId(defaultLangId);
|
||||
setTranslations({
|
||||
[defaultLangId]: {
|
||||
contact: mobile,
|
||||
country_code: countryCode,
|
||||
region_code: regionCode,
|
||||
},
|
||||
});
|
||||
setExtraDetails({
|
||||
[defaultLangId]: {},
|
||||
});
|
||||
|
||||
if (step !== 1) {
|
||||
setStep(1);
|
||||
setDisabledTab({
|
||||
selectCategory: false,
|
||||
details: true,
|
||||
extraDet: true,
|
||||
img: true,
|
||||
loc: true,
|
||||
});
|
||||
}
|
||||
const secondLast = categoryPath.at(-2);
|
||||
if (secondLast) {
|
||||
handleFetchCategories(secondLast);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<BreadCrumb title2={t("adListing")} />
|
||||
<div className="container">
|
||||
<div className="flex flex-col gap-8 mt-8">
|
||||
<h1 className="text-2xl font-medium">{t("adListing")}</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 ${disabledTab?.categoryTab == true ? "opacity-60" : " "
|
||||
}`}
|
||||
onClick={() => handleTabClick(1)}
|
||||
>
|
||||
{t("selectedCategory")}
|
||||
</div>
|
||||
<div
|
||||
className={`transition-all duration-300 p-2 cursor-pointer ${step === 2 ? "bg-primary text-white" : ""
|
||||
} rounded-md ${disabledTab?.detailTab == true ? "opacity-60" : " "
|
||||
}`}
|
||||
onClick={() => handleTabClick(2)}
|
||||
>
|
||||
{t("details")}
|
||||
</div>
|
||||
|
||||
{customFields?.length > 0 && (
|
||||
<div
|
||||
className={`transition-all duration-300 p-2 cursor-pointer ${step === 3 ? "bg-primary text-white" : ""
|
||||
} rounded-md ${disabledTab?.extraDetailTabl == true ? "opacity-60" : " "
|
||||
}`}
|
||||
onClick={() => handleTabClick(3)}
|
||||
>
|
||||
{t("extraDetails")}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
className={`transition-all duration-300 p-2 cursor-pointer ${step === 4 ? "bg-primary text-white" : ""
|
||||
} rounded-md ${disabledTab?.images == true ? "opacity-60" : " "
|
||||
}`}
|
||||
onClick={() => handleTabClick(4)}
|
||||
>
|
||||
{t("images")}
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={`transition-all duration-300 p-2 cursor-pointer ${step === 5 ? "bg-primary text-white" : ""
|
||||
} rounded-md ${disabledTab?.location == true ? "opacity-60" : " "
|
||||
}`}
|
||||
onClick={() => handleTabClick(5)}
|
||||
>
|
||||
{t("location")}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{(step == 2 || (step === 3 && hasTextbox)) && (
|
||||
<AdLanguageSelector
|
||||
langId={langId}
|
||||
setLangId={setLangId}
|
||||
languages={languages}
|
||||
setTranslations={setTranslations}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{(step == 1 || step == 2) && categoryPath?.length > 0 && (
|
||||
<div className="flex flex-col gap-2">
|
||||
<p className="font-medium text-xl">{t("selectedCategory")}</p>
|
||||
<div className="flex">
|
||||
{categoryPath?.map((item, index) => {
|
||||
const shouldShowComma =
|
||||
categoryPath.length > 1 &&
|
||||
index !== categoryPath.length - 1;
|
||||
return (
|
||||
<button
|
||||
key={item.id}
|
||||
className="text-primary ltr:text-left rtl:text-right"
|
||||
onClick={() => handleSelectedTabClick(item?.id)}
|
||||
disabled={categoriesLoading}
|
||||
>
|
||||
{item.translated_name || item.name}
|
||||
{shouldShowComma && ", "}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
{step == 1 && (
|
||||
<ComponentOne
|
||||
categories={categories}
|
||||
setCategoryPath={setCategoryPath}
|
||||
fetchMoreCategory={fetchMoreCategory}
|
||||
lastPage={lastPage}
|
||||
currentPage={currentPage}
|
||||
isLoadMoreCat={isLoadMoreCat}
|
||||
handleCategoryTabClick={handleCategoryTabClick}
|
||||
categoriesLoading={categoriesLoading}
|
||||
/>
|
||||
)}
|
||||
|
||||
{step == 2 && (
|
||||
<ComponentTwo
|
||||
currencies={currencies}
|
||||
setTranslations={setTranslations}
|
||||
current={currentDetails}
|
||||
langId={langId}
|
||||
defaultLangId={defaultLangId}
|
||||
handleDetailsSubmit={handleDetailsSubmit}
|
||||
handleDeatilsBack={handleDeatilsBack}
|
||||
is_job_category={is_job_category}
|
||||
isPriceOptional={isPriceOptional}
|
||||
/>
|
||||
)}
|
||||
|
||||
{step == 3 && (
|
||||
<ComponentThree
|
||||
customFields={customFields}
|
||||
setExtraDetails={setExtraDetails}
|
||||
filePreviews={filePreviews}
|
||||
setFilePreviews={setFilePreviews}
|
||||
setStep={setStep}
|
||||
handleGoBack={handleGoBack}
|
||||
currentExtraDetails={currentExtraDetails}
|
||||
langId={langId}
|
||||
defaultLangId={defaultLangId}
|
||||
/>
|
||||
)}
|
||||
|
||||
{step == 4 && (
|
||||
<ComponentFour
|
||||
uploadedImages={uploadedImages}
|
||||
setUploadedImages={setUploadedImages}
|
||||
otherImages={otherImages}
|
||||
setOtherImages={setOtherImages}
|
||||
setStep={setStep}
|
||||
handleGoBack={handleGoBack}
|
||||
/>
|
||||
)}
|
||||
|
||||
{step == 5 && (
|
||||
<ComponentFive
|
||||
location={location}
|
||||
setLocation={setLocation}
|
||||
handleFullSubmission={handleFullSubmission}
|
||||
isAdPlaced={isAdPlaced}
|
||||
handleGoBack={handleGoBack}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<AdSuccessModal
|
||||
openSuccessModal={openSuccessModal}
|
||||
setOpenSuccessModal={setOpenSuccessModal}
|
||||
createdAdSlug={createdAdSlug}
|
||||
/>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export default Checkauth(AdsListing);
|
||||
173
components/PagesComponent/AdsListing/ComponentFive.jsx
Normal file
173
components/PagesComponent/AdsListing/ComponentFive.jsx
Normal file
@@ -0,0 +1,173 @@
|
||||
import { useState } from "react";
|
||||
import { FaLocationCrosshairs } from "react-icons/fa6";
|
||||
import dynamic from "next/dynamic";
|
||||
import { BiMapPin } from "react-icons/bi";
|
||||
import { IoLocationOutline } from "react-icons/io5";
|
||||
import { toast } from "sonner";
|
||||
import { useSelector } from "react-redux";
|
||||
import ManualAddress from "./ManualAddress";
|
||||
import { getIsBrowserSupported } from "@/redux/reducer/locationSlice";
|
||||
import { getIsPaidApi } from "@/redux/reducer/settingSlice";
|
||||
import { CurrentLanguageData } from "@/redux/reducer/languageSlice";
|
||||
import { t } from "@/utils";
|
||||
import LandingAdEditSearchAutocomplete from "@/components/Location/LandingAdEditSearchAutocomplete";
|
||||
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 ComponentFive = ({
|
||||
location,
|
||||
setLocation,
|
||||
handleFullSubmission,
|
||||
isAdPlaced,
|
||||
handleGoBack,
|
||||
}) => {
|
||||
const CurrentLanguage = useSelector(CurrentLanguageData);
|
||||
const [showManualAddress, setShowManualAddress] = useState(false);
|
||||
const isBrowserSupported = useSelector(getIsBrowserSupported);
|
||||
const [IsGettingCurrentLocation, setIsGettingCurrentLocation] =
|
||||
useState(false);
|
||||
const IsPaidApi = useSelector(getIsPaidApi);
|
||||
const { fetchLocationData } = useGetLocation();
|
||||
|
||||
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"));
|
||||
}
|
||||
};
|
||||
|
||||
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"));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<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 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 disabled:bg-muted-foreground"
|
||||
disabled={isAdPlaced}
|
||||
onClick={handleFullSubmission}
|
||||
>
|
||||
{isAdPlaced ? t("posting") : t("postNow")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<ManualAddress
|
||||
key={showManualAddress}
|
||||
showManualAddress={showManualAddress}
|
||||
setShowManualAddress={setShowManualAddress}
|
||||
setLocation={setLocation}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ComponentFive;
|
||||
233
components/PagesComponent/AdsListing/ComponentFour.jsx
Normal file
233
components/PagesComponent/AdsListing/ComponentFour.jsx
Normal file
@@ -0,0 +1,233 @@
|
||||
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 { toast } from "sonner";
|
||||
import { t } from "@/utils";
|
||||
import CustomImage from "@/components/Common/CustomImage";
|
||||
|
||||
const ComponentFour = ({
|
||||
uploadedImages,
|
||||
setUploadedImages,
|
||||
otherImages,
|
||||
setOtherImages,
|
||||
setStep,
|
||||
handleGoBack,
|
||||
}) => {
|
||||
const onDrop = (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 = () =>
|
||||
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={file.name}
|
||||
/>
|
||||
<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" />
|
||||
</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>
|
||||
));
|
||||
|
||||
const removeImage = (index) => {
|
||||
setUploadedImages((prevImages) => prevImages.filter((_, i) => i !== index));
|
||||
};
|
||||
|
||||
const onOtherDrop = (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]);
|
||||
};
|
||||
|
||||
const {
|
||||
getRootProps: getRootOtheProps,
|
||||
getInputProps: getInputOtherProps,
|
||||
isDragActive: isDragOtherActive,
|
||||
} = useDropzone({
|
||||
onDrop: onOtherDrop,
|
||||
accept: {
|
||||
"image/jpeg": [".jpeg", ".jpg"],
|
||||
"image/png": [".png"],
|
||||
},
|
||||
multiple: true,
|
||||
});
|
||||
|
||||
const removeOtherImage = (index) => {
|
||||
setOtherImages((prevImages) => prevImages.filter((_, i) => i !== index));
|
||||
};
|
||||
|
||||
const filesOther = () =>
|
||||
otherImages &&
|
||||
otherImages?.map((file, index) => (
|
||||
<div key={`${file.name}-${file.size}`} className="relative">
|
||||
<CustomImage
|
||||
width={591}
|
||||
height={350}
|
||||
className="rounded-2xl object-cover aspect-[591/350]"
|
||||
src={URL.createObjectURL(file)}
|
||||
alt={file.name}
|
||||
/>
|
||||
<div className="absolute top-2 left-2 flex gap-2 items-center">
|
||||
<button
|
||||
className="bg-white p-1 rounded-full"
|
||||
onClick={() => removeOtherImage(index)}
|
||||
>
|
||||
<MdClose size={22} color="black" />
|
||||
</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>
|
||||
));
|
||||
|
||||
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-3">
|
||||
<span className="requiredInputLabel text-sm font-semibold">
|
||||
{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-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 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-3">
|
||||
<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 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={() => setStep(5)}
|
||||
>
|
||||
{t("next")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ComponentFour;
|
||||
81
components/PagesComponent/AdsListing/ComponentOne.jsx
Normal file
81
components/PagesComponent/AdsListing/ComponentOne.jsx
Normal file
@@ -0,0 +1,81 @@
|
||||
"use client";
|
||||
import { MdChevronRight } from "react-icons/md";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { t } from "@/utils";
|
||||
import CustomImage from "@/components/Common/CustomImage";
|
||||
|
||||
const ComponentOne = ({
|
||||
categories,
|
||||
categoriesLoading,
|
||||
fetchMoreCategory,
|
||||
lastPage,
|
||||
currentPage,
|
||||
isLoadMoreCat,
|
||||
handleCategoryTabClick,
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<div className=" grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
{categoriesLoading ? (
|
||||
<div className="col-span-12 py-28">
|
||||
<Loader />
|
||||
</div>
|
||||
) : (
|
||||
categories?.map((category) => {
|
||||
return (
|
||||
<div className="" key={category?.id}>
|
||||
<div
|
||||
className="flex justify-between items-center cursor-pointer"
|
||||
key={category?.id}
|
||||
onClick={() => handleCategoryTabClick(category)}
|
||||
>
|
||||
<div className="flex items-center gap-2 ">
|
||||
<CustomImage
|
||||
src={category?.image}
|
||||
alt={category?.translated_name || category?.name}
|
||||
height={48}
|
||||
width={48}
|
||||
className="h-12 w-12 rounded-full"
|
||||
/>
|
||||
<span className="break-all">
|
||||
{category?.translated_name || category?.name}
|
||||
</span>
|
||||
</div>
|
||||
{category?.subcategories?.length > 0 && (
|
||||
<MdChevronRight size={24} className="rtl:scale-x-[-1]" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
|
||||
{lastPage > currentPage && (
|
||||
<div className="text-center mt-6">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="text-sm sm:text-base text-primary w-[256px]"
|
||||
disabled={isLoadMoreCat || categoriesLoading}
|
||||
onClick={fetchMoreCategory}
|
||||
>
|
||||
{isLoadMoreCat ? t("loading") : t("loadMore")}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ComponentOne;
|
||||
|
||||
const Loader = () => {
|
||||
return (
|
||||
<div className="flex justify-center">
|
||||
<div className="relative w-12 h-12">
|
||||
<div className="absolute w-12 h-12 bg-primary rounded-full animate-ping"></div>
|
||||
<div className="absolute w-12 h-12 bg-primary rounded-full animate-ping delay-1000"></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
338
components/PagesComponent/AdsListing/ComponentThree.jsx
Normal file
338
components/PagesComponent/AdsListing/ComponentThree.jsx
Normal file
@@ -0,0 +1,338 @@
|
||||
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 ComponentThree = ({
|
||||
customFields,
|
||||
setExtraDetails,
|
||||
filePreviews,
|
||||
setFilePreviews,
|
||||
setStep,
|
||||
handleGoBack,
|
||||
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((prev) => {
|
||||
const oldUrl = prev?.[langId]?.[id]?.url;
|
||||
if (oldUrl) {
|
||||
URL.revokeObjectURL(oldUrl);
|
||||
}
|
||||
return {
|
||||
...prev,
|
||||
[langId]: {
|
||||
...(prev[langId] || {}),
|
||||
[id]: {
|
||||
url: fileUrl,
|
||||
isPdf: /\.pdf$/i.test(file.name),
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
write(id, file);
|
||||
}
|
||||
};
|
||||
|
||||
const handleChange = (id, value) => write(id, value ?? "");
|
||||
|
||||
const handleCheckboxChange = (id, value, checked) => {
|
||||
const list = currentExtraDetails[id] || [];
|
||||
const next = checked ? [...list, value] : list.filter((v) => v !== value);
|
||||
write(id, next);
|
||||
};
|
||||
|
||||
const renderCustomFields = (field) => {
|
||||
let {
|
||||
id,
|
||||
name,
|
||||
translated_name,
|
||||
type,
|
||||
translated_value,
|
||||
values,
|
||||
min_length,
|
||||
max_length,
|
||||
is_required,
|
||||
} = field;
|
||||
|
||||
const inputProps = {
|
||||
id,
|
||||
name: id,
|
||||
required: !!is_required,
|
||||
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}
|
||||
onValueChange={(value) => handleChange(id, value)}
|
||||
value={currentExtraDetails[id] || ""}
|
||||
>
|
||||
<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 flex-wrap gap-2"
|
||||
>
|
||||
{(translated_value || 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?.[langId]?.[id]?.url;
|
||||
const isPdf = filePreviews?.[langId]?.[id]?.isPdf;
|
||||
|
||||
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>
|
||||
{fileUrl && (
|
||||
<div className="flex items-center gap-1 text-sm flex-nowrap break-words">
|
||||
{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={id}
|
||||
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={() => setStep(4)}
|
||||
>
|
||||
{t("next")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ComponentThree;
|
||||
293
components/PagesComponent/AdsListing/ComponentTwo.jsx
Normal file
293
components/PagesComponent/AdsListing/ComponentTwo.jsx
Normal file
@@ -0,0 +1,293 @@
|
||||
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 ComponentTwo = ({
|
||||
setTranslations,
|
||||
current,
|
||||
langId,
|
||||
handleDetailsSubmit,
|
||||
handleDeatilsBack,
|
||||
is_job_category,
|
||||
isPriceOptional,
|
||||
defaultLangId,
|
||||
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 w-full 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")} //here send param as the one we need to send to the api
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label
|
||||
htmlFor="description"
|
||||
className={langId === defaultLangId ? "requiredInputLabel" : ""}
|
||||
>
|
||||
{t("description")}
|
||||
</Label>
|
||||
<Textarea
|
||||
name="description"
|
||||
id="description"
|
||||
cols="30"
|
||||
rows="3"
|
||||
placeholder={t("enterDescription")}
|
||||
className="border rounded-md px-4 py-2 outline-none"
|
||||
value={current.description || ""}
|
||||
onChange={handleField("description")}
|
||||
></Textarea>
|
||||
</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="Currency" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
{currencies?.map((currency) => (
|
||||
<SelectItem
|
||||
key={currency.id}
|
||||
value={currency.id.toString()}
|
||||
>
|
||||
{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>
|
||||
{/* Render the rest only for default language */}
|
||||
{langId === defaultLangId && (
|
||||
<>
|
||||
{is_job_category ? (
|
||||
<>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="salaryMin">{t("salaryMin")}</Label>
|
||||
<Input
|
||||
type="number"
|
||||
name="salaryMin"
|
||||
id="salaryMin"
|
||||
min={0}
|
||||
placeholder={placeholderLabel}
|
||||
value={current.min_salary || ""}
|
||||
onChange={handleField("min_salary")}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="salaryMax">{t("salaryMax")}</Label>
|
||||
<Input
|
||||
type="number"
|
||||
min={0}
|
||||
name="salaryMax"
|
||||
id="salaryMax"
|
||||
placeholder={placeholderLabel}
|
||||
value={current.max_salary || ""}
|
||||
onChange={handleField("max_salary")}
|
||||
/>
|
||||
</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"
|
||||
min={0}
|
||||
placeholder={placeholderLabel}
|
||||
value={current.price || ""}
|
||||
onChange={handleField("price")}
|
||||
/>
|
||||
</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-muted-foreground text-xs">
|
||||
({t("allowedSlug")})
|
||||
</span>
|
||||
</Label>
|
||||
<Input
|
||||
type="text"
|
||||
name="slug"
|
||||
id="slug"
|
||||
placeholder={t("enterSlug")}
|
||||
value={current.slug || ""}
|
||||
onChange={handleField("slug")}
|
||||
/>
|
||||
</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={handleDeatilsBack}
|
||||
>
|
||||
{t("back")}
|
||||
</button>
|
||||
<button
|
||||
className="bg-primary text-white px-4 py-2 rounded-md text-xl font-light"
|
||||
onClick={handleDetailsSubmit}
|
||||
>
|
||||
{t("next")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ComponentTwo;
|
||||
853
components/PagesComponent/AdsListing/ManualAddress.jsx
Normal file
853
components/PagesComponent/AdsListing/ManualAddress.jsx
Normal file
@@ -0,0 +1,853 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
} from "@/components/ui/command";
|
||||
import { useInView } from "react-intersection-observer";
|
||||
import { Check, ChevronsUpDown, Loader2 } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
getAreasApi,
|
||||
getCitiesApi,
|
||||
getCoutriesApi,
|
||||
getStatesApi,
|
||||
} from "@/utils/api";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { t } from "@/utils";
|
||||
|
||||
const ManualAddress = ({
|
||||
showManualAddress,
|
||||
setShowManualAddress,
|
||||
setLocation,
|
||||
}) => {
|
||||
const [CountryStore, setCountryStore] = useState({
|
||||
Countries: [],
|
||||
SelectedCountry: {},
|
||||
CountrySearch: "",
|
||||
currentPage: 1,
|
||||
hasMore: false,
|
||||
countryOpen: false,
|
||||
isLoading: false,
|
||||
});
|
||||
const [StateStore, setStateStore] = useState({
|
||||
States: [],
|
||||
SelectedState: {},
|
||||
StateSearch: "",
|
||||
currentPage: 1,
|
||||
hasMore: false,
|
||||
stateOpen: false,
|
||||
isLoading: false,
|
||||
});
|
||||
const [CityStore, setCityStore] = useState({
|
||||
Cities: [],
|
||||
SelectedCity: {},
|
||||
CitySearch: "",
|
||||
currentPage: 1,
|
||||
hasMore: false,
|
||||
isLoading: false,
|
||||
cityOpen: false,
|
||||
});
|
||||
const [AreaStore, setAreaStore] = useState({
|
||||
Areas: [],
|
||||
SelectedArea: {},
|
||||
AreaSearch: "",
|
||||
currentPage: 1,
|
||||
hasMore: false,
|
||||
areaOpen: false,
|
||||
isLoading: false,
|
||||
});
|
||||
const [Address, setAddress] = useState("");
|
||||
const [fieldErrors, setFieldErrors] = useState({});
|
||||
const isCountrySelected =
|
||||
Object.keys(CountryStore?.SelectedCountry).length > 0;
|
||||
// Check if areas exist for the selected city
|
||||
const hasAreas = AreaStore?.Areas.length > 0;
|
||||
// Infinite scroll refs
|
||||
const { ref: stateRef, inView: stateInView } = useInView();
|
||||
const { ref: countryRef, inView: countryInView } = useInView();
|
||||
const { ref: cityRef, inView: cityInView } = useInView();
|
||||
const { ref: areaRef, inView: areaInView } = useInView();
|
||||
|
||||
const getCountriesData = async (search, page) => {
|
||||
try {
|
||||
setCountryStore((prev) => ({
|
||||
...prev,
|
||||
isLoading: true,
|
||||
Countries: search ? [] : prev.Countries, // Clear list if searching
|
||||
}));
|
||||
// Fetch countries
|
||||
const params = {};
|
||||
if (search) {
|
||||
params.search = search; // Send only 'search' if provided
|
||||
} else {
|
||||
params.page = page; // Send only 'page' if no search
|
||||
}
|
||||
|
||||
const res = await getCoutriesApi.getCoutries(params);
|
||||
let allCountries;
|
||||
if (page > 1) {
|
||||
allCountries = [...CountryStore?.Countries, ...res?.data?.data?.data];
|
||||
} else {
|
||||
allCountries = res?.data?.data?.data;
|
||||
}
|
||||
setCountryStore((prev) => ({
|
||||
...prev,
|
||||
currentPage: res?.data?.data?.current_page,
|
||||
Countries: allCountries,
|
||||
hasMore:
|
||||
res?.data?.data?.current_page < res?.data?.data?.last_page
|
||||
? true
|
||||
: false,
|
||||
isLoading: false,
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error("Error fetching countries data:", error);
|
||||
setCountryStore((prev) => ({ ...prev, isLoading: false }));
|
||||
}
|
||||
};
|
||||
const getStatesData = async (search, page) => {
|
||||
try {
|
||||
setStateStore((prev) => ({
|
||||
...prev,
|
||||
isLoading: true,
|
||||
States: search ? [] : prev.States,
|
||||
}));
|
||||
const params = {
|
||||
country_id: CountryStore?.SelectedCountry?.id,
|
||||
};
|
||||
if (search) {
|
||||
params.search = search; // Send only 'search' if provided
|
||||
} else {
|
||||
params.page = page; // Send only 'page' if no search
|
||||
}
|
||||
|
||||
const res = await getStatesApi.getStates(params);
|
||||
|
||||
let allStates;
|
||||
if (page > 1) {
|
||||
allStates = [...StateStore?.States, ...res?.data?.data?.data];
|
||||
} else {
|
||||
allStates = res?.data?.data?.data;
|
||||
}
|
||||
|
||||
setStateStore((prev) => ({
|
||||
...prev,
|
||||
currentPage: res?.data?.data?.current_page,
|
||||
States: allStates,
|
||||
hasMore:
|
||||
res?.data?.data?.current_page < res?.data?.data?.last_page
|
||||
? true
|
||||
: false,
|
||||
isLoading: false,
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error("Error fetching states data:", error);
|
||||
setStateStore((prev) => ({ ...prev, isLoading: false }));
|
||||
return [];
|
||||
}
|
||||
};
|
||||
const getCitiesData = async (search, page) => {
|
||||
try {
|
||||
setCityStore((prev) => ({
|
||||
...prev,
|
||||
isLoading: true,
|
||||
Cities: search ? [] : prev.Cities,
|
||||
}));
|
||||
const params = {
|
||||
state_id: StateStore?.SelectedState?.id,
|
||||
};
|
||||
if (search) {
|
||||
params.search = search; // Send only 'search' if provided
|
||||
} else {
|
||||
params.page = page; // Send only 'page' if no search
|
||||
}
|
||||
|
||||
const res = await getCitiesApi.getCities(params);
|
||||
let allCities;
|
||||
if (page > 1) {
|
||||
allCities = [...CityStore?.Cities, ...res?.data?.data?.data];
|
||||
} else {
|
||||
allCities = res?.data?.data?.data;
|
||||
}
|
||||
setCityStore((prev) => ({
|
||||
...prev,
|
||||
currentPage: res?.data?.data?.current_page,
|
||||
Cities: allCities,
|
||||
hasMore:
|
||||
res?.data?.data?.current_page < res?.data?.data?.last_page
|
||||
? true
|
||||
: false,
|
||||
isLoading: false,
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error("Error fetching cities data:", error);
|
||||
setCityStore((prev) => ({ ...prev, isLoading: false }));
|
||||
return [];
|
||||
}
|
||||
};
|
||||
const getAreaData = async (search, page) => {
|
||||
try {
|
||||
setAreaStore((prev) => ({
|
||||
...prev,
|
||||
isLoading: true,
|
||||
Areas: search ? [] : prev.Areas,
|
||||
}));
|
||||
const params = {
|
||||
city_id: CityStore?.SelectedCity?.id,
|
||||
};
|
||||
if (search) {
|
||||
params.search = search;
|
||||
} else {
|
||||
params.page = page;
|
||||
}
|
||||
const res = await getAreasApi.getAreas(params);
|
||||
let allArea;
|
||||
if (page > 1) {
|
||||
allArea = [...AreaStore?.Areas, ...res?.data?.data?.data];
|
||||
} else {
|
||||
allArea = res?.data?.data?.data;
|
||||
}
|
||||
setAreaStore((prev) => ({
|
||||
...prev,
|
||||
currentPage: res?.data?.data?.current_page,
|
||||
Areas: allArea,
|
||||
hasMore:
|
||||
res?.data?.data?.current_page < res?.data?.data?.last_page
|
||||
? true
|
||||
: false,
|
||||
isLoading: false,
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error("Error fetching areas data:", error);
|
||||
setAreaStore((prev) => ({ ...prev, isLoading: false }));
|
||||
return [];
|
||||
}
|
||||
};
|
||||
useEffect(() => {
|
||||
const timeout = setTimeout(() => {
|
||||
if (showManualAddress) {
|
||||
getCountriesData(CountryStore?.CountrySearch, 1);
|
||||
}
|
||||
}, 500);
|
||||
return () => {
|
||||
clearTimeout(timeout);
|
||||
};
|
||||
}, [CountryStore?.CountrySearch, showManualAddress]);
|
||||
useEffect(() => {
|
||||
if (CountryStore?.SelectedCountry?.id) {
|
||||
const timeout = setTimeout(() => {
|
||||
getStatesData(StateStore?.StateSearch, 1);
|
||||
}, 500);
|
||||
return () => {
|
||||
clearTimeout(timeout);
|
||||
};
|
||||
}
|
||||
}, [CountryStore?.SelectedCountry?.id, StateStore?.StateSearch]);
|
||||
useEffect(() => {
|
||||
if (StateStore?.SelectedState?.id) {
|
||||
const timeout = setTimeout(() => {
|
||||
getCitiesData(CityStore?.CitySearch, 1);
|
||||
}, 500);
|
||||
return () => {
|
||||
clearTimeout(timeout);
|
||||
};
|
||||
}
|
||||
}, [StateStore?.SelectedState?.id, CityStore?.CitySearch]);
|
||||
useEffect(() => {
|
||||
if (CityStore?.SelectedCity?.id) {
|
||||
const timeout = setTimeout(() => {
|
||||
getAreaData(AreaStore?.AreaSearch, 1);
|
||||
}, 500);
|
||||
return () => {
|
||||
clearTimeout(timeout);
|
||||
};
|
||||
}
|
||||
}, [CityStore?.SelectedCity?.id, AreaStore?.AreaSearch]);
|
||||
|
||||
// Trigger infinite scroll when refs come into view
|
||||
useEffect(() => {
|
||||
if (CountryStore?.hasMore && !CountryStore?.isLoading && countryInView) {
|
||||
getCountriesData("", CountryStore?.currentPage + 1);
|
||||
}
|
||||
}, [
|
||||
countryInView,
|
||||
CountryStore?.hasMore,
|
||||
CountryStore?.isLoading,
|
||||
CountryStore?.currentPage,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (StateStore?.hasMore && !StateStore?.isLoading && stateInView) {
|
||||
getStatesData("", StateStore?.currentPage + 1);
|
||||
}
|
||||
}, [
|
||||
stateInView,
|
||||
StateStore?.hasMore,
|
||||
StateStore?.isLoading,
|
||||
StateStore?.currentPage,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (CityStore?.hasMore && !CityStore?.isLoading && cityInView) {
|
||||
getCitiesData("", CityStore?.currentPage + 1);
|
||||
}
|
||||
}, [
|
||||
cityInView,
|
||||
CityStore?.hasMore,
|
||||
CityStore?.isLoading,
|
||||
CityStore?.currentPage,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (AreaStore?.hasMore && !AreaStore?.isLoading && areaInView) {
|
||||
getAreaData("", AreaStore?.currentPage + 1);
|
||||
}
|
||||
}, [
|
||||
areaInView,
|
||||
AreaStore?.hasMore,
|
||||
AreaStore?.isLoading,
|
||||
AreaStore?.currentPage,
|
||||
]);
|
||||
|
||||
const validateFields = () => {
|
||||
const errors = {};
|
||||
if (!CountryStore?.SelectedCountry?.name) errors.country = true;
|
||||
if (!StateStore?.SelectedState?.name) errors.state = true;
|
||||
if (!CityStore?.SelectedCity?.name) errors.city = true;
|
||||
if (!AreaStore?.SelectedArea?.name && !Address.trim())
|
||||
errors.address = true;
|
||||
return errors;
|
||||
};
|
||||
|
||||
const handleCountryChange = (value) => {
|
||||
const Country = CountryStore?.Countries.find(
|
||||
(country) => country.name === value
|
||||
);
|
||||
setCountryStore((prev) => ({
|
||||
...prev,
|
||||
SelectedCountry: Country,
|
||||
countryOpen: false,
|
||||
}));
|
||||
setStateStore({
|
||||
States: [],
|
||||
SelectedState: {},
|
||||
StateSearch: "",
|
||||
currentPage: 1,
|
||||
hasMore: false,
|
||||
stateOpen: false,
|
||||
});
|
||||
setCityStore({
|
||||
Cities: [],
|
||||
SelectedCity: {},
|
||||
CitySearch: "",
|
||||
currentPage: 1,
|
||||
hasMore: false,
|
||||
cityOpen: false,
|
||||
});
|
||||
setAreaStore({
|
||||
Areas: [],
|
||||
SelectedArea: {},
|
||||
AreaSearch: "",
|
||||
currentPage: 1,
|
||||
hasMore: false,
|
||||
areaOpen: false,
|
||||
});
|
||||
setAddress("");
|
||||
};
|
||||
const handleStateChange = (value) => {
|
||||
const State = StateStore?.States.find((state) => state.name === value);
|
||||
setStateStore((prev) => ({
|
||||
...prev,
|
||||
SelectedState: State,
|
||||
stateOpen: false,
|
||||
}));
|
||||
setCityStore({
|
||||
Cities: [],
|
||||
SelectedCity: {},
|
||||
CitySearch: "",
|
||||
currentPage: 1,
|
||||
hasMore: false,
|
||||
cityOpen: false,
|
||||
});
|
||||
setAreaStore({
|
||||
Areas: [],
|
||||
SelectedArea: {},
|
||||
AreaSearch: "",
|
||||
currentPage: 1,
|
||||
hasMore: false,
|
||||
areaOpen: false,
|
||||
});
|
||||
setAddress("");
|
||||
};
|
||||
const handleCityChange = (value) => {
|
||||
const City = CityStore?.Cities.find((city) => city.name === value);
|
||||
setCityStore((prev) => ({
|
||||
...prev,
|
||||
SelectedCity: City,
|
||||
cityOpen: false,
|
||||
}));
|
||||
setAreaStore({
|
||||
Areas: [],
|
||||
SelectedArea: {},
|
||||
AreaSearch: "",
|
||||
currentPage: 1,
|
||||
hasMore: false,
|
||||
areaOpen: false,
|
||||
});
|
||||
setAddress("");
|
||||
};
|
||||
const handleAreaChange = (value) => {
|
||||
const chosenArea = AreaStore?.Areas.find((item) => item.name === value);
|
||||
setAreaStore((prev) => ({
|
||||
...prev,
|
||||
SelectedArea: chosenArea,
|
||||
areaOpen: false,
|
||||
}));
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
const errors = validateFields();
|
||||
setFieldErrors(errors);
|
||||
if (Object.keys(errors).length > 0) return;
|
||||
|
||||
// Build address parts array and filter out empty values
|
||||
|
||||
const addressParts = [];
|
||||
const addressPartsTranslated = [];
|
||||
|
||||
if (hasAreas && AreaStore?.SelectedArea?.name) {
|
||||
addressParts.push(AreaStore.SelectedArea.name);
|
||||
addressPartsTranslated.push(
|
||||
AreaStore.SelectedArea.translated_name || AreaStore.SelectedArea.name
|
||||
);
|
||||
} else if (Address.trim()) {
|
||||
addressParts.push(Address.trim());
|
||||
addressPartsTranslated.push(Address.trim());
|
||||
}
|
||||
if (CityStore?.SelectedCity?.name) {
|
||||
addressParts.push(CityStore.SelectedCity.name);
|
||||
addressPartsTranslated.push(
|
||||
CityStore.SelectedCity.translated_name || CityStore.SelectedCity.name
|
||||
);
|
||||
}
|
||||
if (StateStore?.SelectedState?.name) {
|
||||
addressParts.push(StateStore.SelectedState.name);
|
||||
addressPartsTranslated.push(
|
||||
StateStore.SelectedState.translated_name ||
|
||||
StateStore.SelectedState.name
|
||||
);
|
||||
}
|
||||
if (CountryStore?.SelectedCountry?.name) {
|
||||
addressParts.push(CountryStore.SelectedCountry.name);
|
||||
addressPartsTranslated.push(
|
||||
CountryStore.SelectedCountry.translated_name ||
|
||||
CountryStore.SelectedCountry.name
|
||||
);
|
||||
}
|
||||
|
||||
const formattedAddress = addressParts.join(", ");
|
||||
const formattedAddressTranslated = addressPartsTranslated.join(", ");
|
||||
|
||||
const locationData = {
|
||||
country: CountryStore?.SelectedCountry?.name || "",
|
||||
state: StateStore?.SelectedState?.name || "",
|
||||
city: CityStore?.SelectedCity?.name || "",
|
||||
formattedAddress: formattedAddress,
|
||||
address_translated: formattedAddressTranslated,
|
||||
lat: CityStore?.SelectedCity?.latitude || null,
|
||||
long: CityStore?.SelectedCity?.longitude || null,
|
||||
area_id: AreaStore?.SelectedArea?.id || null,
|
||||
};
|
||||
setLocation(locationData);
|
||||
setShowManualAddress(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={showManualAddress} onOpenChange={setShowManualAddress}>
|
||||
<DialogContent className="">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t("manuAddAddress")}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="flex flex-col gap-4 py-4">
|
||||
<div className="flex flex-col gap-1">
|
||||
<Popover
|
||||
modal
|
||||
open={CountryStore?.countryOpen}
|
||||
onOpenChange={(isOpen) =>
|
||||
setCountryStore((prev) => ({ ...prev, countryOpen: isOpen }))
|
||||
}
|
||||
>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={CountryStore?.countryOpen}
|
||||
className={`w-full justify-between outline-none ${fieldErrors.country ? "border-red-500" : ""
|
||||
}`}
|
||||
>
|
||||
{CountryStore?.SelectedCountry?.translated_name ||
|
||||
CountryStore?.SelectedCountry?.name ||
|
||||
t("country")}
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
{fieldErrors.country && (
|
||||
<span className="text-red-500 text-sm">
|
||||
{t("countryRequired")}
|
||||
</span>
|
||||
)}
|
||||
<PopoverContent className="w-[var(--radix-popover-trigger-width)] p-0">
|
||||
<Command shouldFilter={false}>
|
||||
<CommandInput
|
||||
placeholder={t("searchCountries")}
|
||||
value={CountryStore.CountrySearch || ""}
|
||||
onValueChange={(val) => {
|
||||
setCountryStore((prev) => ({
|
||||
...prev,
|
||||
CountrySearch: val,
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
<CommandEmpty>
|
||||
{CountryStore.isLoading ? (
|
||||
<LoacationLoader />
|
||||
) : (
|
||||
<div className="text-center">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t("noCountriesFound")}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</CommandEmpty>
|
||||
<CommandGroup className="max-h-[240px] overflow-y-auto">
|
||||
{CountryStore?.Countries?.map((country, index) => {
|
||||
const isLast =
|
||||
index === CountryStore?.Countries?.length - 1;
|
||||
return (
|
||||
<CommandItem
|
||||
key={country.id}
|
||||
value={country.name}
|
||||
onSelect={handleCountryChange}
|
||||
ref={isLast ? countryRef : null}
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
"mr-2 h-4 w-4",
|
||||
CountryStore?.SelectedCountry?.name ===
|
||||
country?.name
|
||||
? "opacity-100"
|
||||
: "opacity-0"
|
||||
)}
|
||||
/>
|
||||
{country.translated_name || country.name}
|
||||
</CommandItem>
|
||||
);
|
||||
})}
|
||||
{CountryStore.isLoading &&
|
||||
CountryStore.Countries.length > 0 && <LoacationLoader />}
|
||||
</CommandGroup>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
<Popover
|
||||
modal
|
||||
open={StateStore?.stateOpen}
|
||||
onOpenChange={(isOpen) =>
|
||||
setStateStore((prev) => ({ ...prev, stateOpen: isOpen }))
|
||||
}
|
||||
>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={StateStore?.stateOpen}
|
||||
className={`w-full justify-between outline-none ${fieldErrors.state ? "border-red-500" : ""
|
||||
}`}
|
||||
disabled={!isCountrySelected}
|
||||
>
|
||||
{StateStore?.SelectedState?.translated_name ||
|
||||
StateStore?.SelectedState?.name ||
|
||||
t("state")}
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
{fieldErrors.state && (
|
||||
<span className="text-red-500 text-sm">
|
||||
{t("stateRequired")}
|
||||
</span>
|
||||
)}
|
||||
<PopoverContent className="w-[var(--radix-popover-trigger-width)] p-0">
|
||||
<Command shouldFilter={false}>
|
||||
<CommandInput
|
||||
placeholder={t("searchStates")}
|
||||
value={StateStore.StateSearch || ""}
|
||||
onValueChange={(val) => {
|
||||
setStateStore((prev) => ({ ...prev, StateSearch: val }));
|
||||
}}
|
||||
/>
|
||||
<CommandEmpty>
|
||||
{StateStore.isLoading ? (
|
||||
<LoacationLoader />
|
||||
) : (
|
||||
<div className="text-center">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t("noStatesFound")}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</CommandEmpty>
|
||||
<CommandGroup className="max-h-[240px] overflow-y-auto">
|
||||
{StateStore?.States?.map((state, index) => {
|
||||
const isLast = index === StateStore?.States?.length - 1;
|
||||
return (
|
||||
<CommandItem
|
||||
key={state.id}
|
||||
value={state.name}
|
||||
onSelect={handleStateChange}
|
||||
ref={isLast ? stateRef : null}
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
"mr-2 h-4 w-4",
|
||||
StateStore?.SelectedState?.name === state?.name
|
||||
? "opacity-100"
|
||||
: "opacity-0"
|
||||
)}
|
||||
/>
|
||||
{state.translated_name || state.name}
|
||||
</CommandItem>
|
||||
);
|
||||
})}
|
||||
{StateStore.isLoading && StateStore.States.length > 0 && (
|
||||
<LoacationLoader />
|
||||
)}
|
||||
</CommandGroup>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
<Popover
|
||||
modal
|
||||
open={CityStore?.cityOpen}
|
||||
onOpenChange={(isOpen) =>
|
||||
setCityStore((prev) => ({ ...prev, cityOpen: isOpen }))
|
||||
}
|
||||
>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={CityStore?.cityOpen}
|
||||
className={`w-full justify-between outline-none ${fieldErrors.city ? "border-red-500" : ""
|
||||
}`}
|
||||
disabled={!StateStore?.SelectedState?.id}
|
||||
>
|
||||
{CityStore?.SelectedCity?.translated_name ||
|
||||
CityStore?.SelectedCity?.name ||
|
||||
t("city")}
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
{fieldErrors.city && (
|
||||
<span className="text-red-500 text-sm">
|
||||
{t("cityRequired")}
|
||||
</span>
|
||||
)}
|
||||
<PopoverContent className="w-[var(--radix-popover-trigger-width)] p-0">
|
||||
<Command shouldFilter={false}>
|
||||
<CommandInput
|
||||
placeholder={t("searchCities")}
|
||||
value={CityStore.CitySearch || ""}
|
||||
onValueChange={(val) => {
|
||||
setCityStore((prev) => ({ ...prev, CitySearch: val }));
|
||||
}}
|
||||
/>
|
||||
<CommandEmpty>
|
||||
{CityStore.isLoading ? (
|
||||
<LoacationLoader />
|
||||
) : (
|
||||
<div className="text-center">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t("noCitiesFound")}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</CommandEmpty>
|
||||
<CommandGroup className="max-h-[240px] overflow-y-auto">
|
||||
{CityStore?.Cities?.map((city, index) => {
|
||||
const isLast = index === CityStore?.Cities?.length - 1;
|
||||
return (
|
||||
<CommandItem
|
||||
key={city.id}
|
||||
value={city.name}
|
||||
onSelect={handleCityChange}
|
||||
ref={isLast ? cityRef : null}
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
"mr-2 h-4 w-4",
|
||||
CityStore?.SelectedCity?.name === city?.name
|
||||
? "opacity-100"
|
||||
: "opacity-0"
|
||||
)}
|
||||
/>
|
||||
{city.translated_name || city.name}
|
||||
</CommandItem>
|
||||
);
|
||||
})}
|
||||
{CityStore.isLoading && CityStore.Cities.length > 0 && (
|
||||
<LoacationLoader />
|
||||
)}
|
||||
</CommandGroup>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
|
||||
{hasAreas || AreaStore?.AreaSearch ? (
|
||||
<div className="flex flex-col gap-1">
|
||||
<Popover
|
||||
modal
|
||||
open={AreaStore?.areaOpen}
|
||||
onOpenChange={(isOpen) =>
|
||||
setAreaStore((prev) => ({ ...prev, areaOpen: isOpen }))
|
||||
}
|
||||
>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={AreaStore?.areaOpen}
|
||||
className={`w-full justify-between outline-none ${fieldErrors.address ? "border-red-500" : ""
|
||||
}`}
|
||||
disabled={!CityStore?.SelectedCity?.id}
|
||||
>
|
||||
{AreaStore?.SelectedArea?.translated_name ||
|
||||
AreaStore?.SelectedArea?.name ||
|
||||
t("area")}
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
{fieldErrors.address && (
|
||||
<span className="text-red-500 text-sm">
|
||||
{t("areaRequired")}
|
||||
</span>
|
||||
)}
|
||||
<PopoverContent className="w-[var(--radix-popover-trigger-width)] p-0">
|
||||
<Command shouldFilter={false}>
|
||||
<CommandInput
|
||||
placeholder={t("searchAreas")}
|
||||
value={AreaStore.AreaSearch || ""}
|
||||
onValueChange={(val) => {
|
||||
setAreaStore((prev) => ({ ...prev, AreaSearch: val }));
|
||||
}}
|
||||
/>
|
||||
<CommandEmpty>
|
||||
{AreaStore.isLoading ? (
|
||||
<LoacationLoader />
|
||||
) : (
|
||||
<div className="text-center">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t("noAreasFound")}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</CommandEmpty>
|
||||
<CommandGroup className="max-h-[240px] overflow-y-auto">
|
||||
{AreaStore?.Areas?.map((area, index) => {
|
||||
const isLast = index === AreaStore?.Areas?.length - 1;
|
||||
return (
|
||||
<CommandItem
|
||||
key={area.id}
|
||||
value={area.name}
|
||||
onSelect={handleAreaChange}
|
||||
ref={isLast ? areaRef : null}
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
"mr-2 h-4 w-4",
|
||||
AreaStore?.SelectedArea?.name === area?.name
|
||||
? "opacity-100"
|
||||
: "opacity-0"
|
||||
)}
|
||||
/>
|
||||
{area.translated_name || area.name}
|
||||
</CommandItem>
|
||||
);
|
||||
})}
|
||||
{AreaStore.isLoading && AreaStore.Areas.length > 0 && (
|
||||
<LoacationLoader />
|
||||
)}
|
||||
</CommandGroup>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col gap-1">
|
||||
<Textarea
|
||||
rows={5}
|
||||
className={`border p-2 outline-none rounded-md w-full ${fieldErrors.address ? "border-red-500" : ""
|
||||
}`}
|
||||
placeholder={t("enterAddre")}
|
||||
value={Address}
|
||||
onChange={(e) => setAddress(e.target.value)}
|
||||
disabled={!CityStore?.SelectedCity?.id}
|
||||
/>
|
||||
{fieldErrors.address && (
|
||||
<span className="text-red-500 text-sm">
|
||||
{t("addressRequired")}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<DialogFooter className="flex justify-end gap-2">
|
||||
<button onClick={() => setShowManualAddress(false)}>
|
||||
{t("cancel")}
|
||||
</button>
|
||||
<button
|
||||
className="bg-primary p-2 px-4 rounded-md text-white font-medium"
|
||||
onClick={handleSave}
|
||||
>
|
||||
{t("save")}
|
||||
</button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default ManualAddress;
|
||||
|
||||
const LoacationLoader = () => {
|
||||
return (
|
||||
<div className="flex items-center justify-center py-4">
|
||||
<Loader2 className="size-4 animate-spin" />
|
||||
<span className="ml-2 text-sm text-muted-foreground">
|
||||
{t("loading")}..
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user