ISSUE 3: add image uploader and include add FAQ logic

This commit is contained in:
Ayobami
2025-06-30 22:17:18 +01:00
parent 6612336460
commit 05cd9c8318
+302 -149
View File
@@ -13,7 +13,12 @@ import MkdSDK from "@/utils/MkdSDK";
import useDelayUnmount from "@/hooks/useDelayUnmount";
import { useContext } from "react";
import { GlobalContext, showToast } from "@/globalContext";
import { DRAFT_STATUS, IMAGE_STATUS, SPACE_STATUS, SPACE_VISIBILITY } from "@/utils/constants";
import {
DRAFT_STATUS,
IMAGE_STATUS,
SPACE_STATUS,
SPACE_VISIBILITY,
} from "@/utils/constants";
import { Link } from "react-router-dom";
import CustomSelectV2 from "@/components/CustomSelectV2";
import useCancellation from "@/hooks/api/useCancellation";
@@ -110,14 +115,30 @@ const SpaceDetailsTwo = () => {
for (let i = 0; i < pictures.length; i++) {
const file = pictures[i];
const allowedTypes = ['image/jpeg', 'image/png', 'image/webp', 'image/svg+xml'];
const allowedTypes = [
"image/jpeg",
"image/png",
"image/webp",
"image/svg+xml",
];
if (file?.type && !allowedTypes.includes(file?.type)) {
showToast(globalDispatch, 'Invalid file type. Only JPEG, PNG, WEBP, and SVG are allowed.', 4000, "ERROR");
showToast(
globalDispatch,
"Invalid file type. Only JPEG, PNG, WEBP, and SVG are allowed.",
4000,
"ERROR"
);
return;
}
if (file?.size && file?.size > 5 * 1024 * 1024) { // 5 MB limit
showToast(globalDispatch, 'One of the image is too large. Max size is 5 MB.', 4000, "ERROR");
if (file?.size && file?.size > 5 * 1024 * 1024) {
// 5 MB limit
showToast(
globalDispatch,
"One or more of the image is too large. Max size is 5 MB.",
4000,
"ERROR"
);
return;
}
@@ -128,7 +149,16 @@ const SpaceDetailsTwo = () => {
dispatch({ type: "SET_THUMBNAIL", payload: upload.id });
}
}
dispatch({ type: "SET_DETAILS_TWO", payload: { faqs: data.faqs, amenities: data.amenities, addons: data.addons, pictures: uploadedImages, pictureIds: uploadedIds } });
dispatch({
type: "SET_DETAILS_TWO",
payload: {
faqs: data.faqs,
amenities: data.amenities,
addons: data.addons,
pictures: uploadedImages,
pictureIds: uploadedIds,
},
});
globalDispatch({ type: "STOP_LOADING" });
navigate("/spaces/add/3");
@@ -156,7 +186,7 @@ const SpaceDetailsTwo = () => {
name: spaceData.name,
rule: spaceData.rule,
},
"POST",
"POST"
);
dispatch({ type: "SET_PROPERTY_ID", payload: propertyResult.message });
}
@@ -177,7 +207,7 @@ const SpaceDetailsTwo = () => {
additional_guest_rate: spaceData.additional_guest_rate || undefined,
size: spaceData.size || undefined,
},
"POST",
"POST"
);
}
@@ -190,7 +220,7 @@ const SpaceDetailsTwo = () => {
property_id: propertyResult?.message ?? spaceData.property_id,
add_on_id: addon_id,
},
"POST",
"POST"
);
}
@@ -208,7 +238,7 @@ const SpaceDetailsTwo = () => {
photo_id: upload.id,
is_approved: IMAGE_STATUS.IN_REVIEW,
},
"POST",
"POST"
);
}
if (file?.name == formValues.thumbnail) {
@@ -219,7 +249,7 @@ const SpaceDetailsTwo = () => {
default_image_id: upload.id,
// is_approved: IMAGE_STATUS.APPROVED,
},
"PUT",
"PUT"
);
}
}
@@ -234,7 +264,7 @@ const SpaceDetailsTwo = () => {
question: faq.question,
answer: faq.answer,
},
"POST",
"POST"
);
}
@@ -247,7 +277,7 @@ const SpaceDetailsTwo = () => {
property_spaces_id: propertySpaceResult.message,
amenity_id,
},
"POST",
"POST"
);
}
navigate("/account/my-spaces");
@@ -278,138 +308,228 @@ const SpaceDetailsTwo = () => {
setPictures((prev) => {
var copy = [...prev];
copy[i] = picFile;
return copy;
return copy.filter(Boolean);
});
});
}
}, []);
function isCatOthers() {
const cat = spaceCategories.find((cat) => Number(cat.id) == Number(spaceData.category))
const cat = spaceCategories.find(
(cat) => Number(cat.id) == Number(spaceData.category)
);
if (cat?.category === "Others") {
return true
} else return false
return true;
} else return false;
}
return (
<div className="min-h-screen pb-40 md:max-w-[656px]">
<form
onSubmit={handleSubmit(onSubmit)}
autoComplete="off"
>
<h1 className="mb-8 text-3xl font-bold md:text-4xl">Space Details</h1>
<div className="text-sm md:px-[20px] md:py-[32px]">
<h3 className="text-xl font-semibold md:text-2xl">* Photographs of the space</h3>
<p className="mb-8">file type (jpeg/png/svg), max size (5MB), suggest resolution (640*480)</p>
<div className="eighteen-step-image mb-8 flex flex-wrap justify-center gap-x-2 gap-y-4 md:gap-5">
<div className='min-h-screen pb-40 md:max-w-[656px]'>
<form onSubmit={handleSubmit(onSubmit)} autoComplete='off'>
<h1 className='mb-8 text-3xl font-bold md:text-4xl'>Space Details</h1>
<div className='text-sm md:px-[20px] md:py-[32px]'>
<h3 className='text-xl font-semibold md:text-2xl'>
* Photographs of the space
</h3>
<p className='mb-8'>
file type (jpeg/png/svg), max size (5MB), suggest resolution
(640*480)
</p>
<div className='mb-8 flex flex-col justify-center gap-x-2 gap-y-4 md:gap-5'>
{/* FileUploader for images */}
<FileUploader
multiple
handleChange={(files) => {
const fileArray =
files instanceof FileList ? Array.from(files) : [files];
// Enforce max 6 images
if (pictures.length + fileArray.length > 6) {
showToast(
globalDispatch,
"You can only upload up to 6 images.",
4000,
"ERROR"
);
return;
}
// Enforce 1MB per image
for (let file of fileArray) {
if (file.size > 1024 * 1024) {
showToast(
globalDispatch,
"Each image must not exceed 1MB.",
4000,
"ERROR"
);
return;
}
}
setPictures((prev) => [...prev, ...fileArray]);
}}
name='images'
types={["JPG", "PNG", "JPEG", "SVG", "WEBP"]}
maxSize={1}
/>
{/* Show previews */}
<div className='eighteen-step-image flex flex-wrap gap-2'>
{pictures.map((file, idx) => {
// add FileUploader logic here
const imageUrl =
file instanceof File ? URL.createObjectURL(file) : file;
return (
<div
key={idx}
className='relative flex flex-col items-center'
>
<img
src={imageUrl}
alt={`preview-${idx + 1}`}
className='mb-2 h-24 w-24 rounded border object-cover'
/>
<button
type='button'
className='text-xs text-red-500 underline'
onClick={() =>
setPictures((prev) => prev.filter((_, i) => i !== idx))
}
>
Remove
</button>
</div>
);
})}
</div>
<h3 className="mb-4 text-xl font-bold">* Select thumbnail image</h3>
</div>
<h3 className='mb-4 text-xl font-bold'>* Select thumbnail image</h3>
<CustomSelectV2
items={pictures.filter((pic) => pic?.name)}
labelField="name"
valueField="name"
containerClassName="mb-12"
className="w-full border py-2 px-3 focus:outline-primary"
openClassName="ring-primary ring-2"
labelField='name'
valueField='name'
containerClassName='mb-12'
className='w-full border px-3 py-2 focus:outline-primary'
openClassName='ring-primary ring-2'
placeholder={"Select thumbnail"}
control={control}
name="thumbnail"
name='thumbnail'
/>
<h3 className="mb-4 text-xl font-bold">
What do you offer with the space <span className="text-sm font-normal italic text-gray-500">(optional)</span>
<h3 className='mb-4 text-xl font-bold'>
What do you offer with the space{" "}
<span className='text-sm font-normal italic text-gray-500'>
(optional)
</span>
</h3>
<div className="flex gap-3 items-center">
<div className='flex items-center gap-3'>
<button
type="button"
className="mb-2 font-bold text-[#1570EF]"
type='button'
className='mb-2 font-bold text-[#1570EF]'
onClick={() => setAddAmenitiesPopup(true)}
>
+ Select items
</button>
</div>
<div className="addons-grid mb-12">
<div className='addons-grid mb-12'>
{amenities
?.filter((am) => {
if (Array.isArray(selectedAmenities)) {
return selectedAmenities?.includes(String(am.id));
}
return false;
}).sort((a, b) => (a.space_id === null ? -1 : 1) - (b.space_id === null ? -1 : 1))
})
.sort(
(a, b) =>
(a.space_id === null ? -1 : 1) -
(b.space_id === null ? -1 : 1)
)
.map((am) => (
<li
className="flex w-fit items-center gap-2 mb-4 sm:mb-0"
key={am.id}>
className='mb-4 flex w-fit items-center gap-2 sm:mb-0'
key={am.id}
>
<CircleCheckIcon />
{am.name}
</li>
))}
</div>
<h3 className="mb-4 text-xl font-bold">
Add-ons <span className="text-sm font-normal italic text-gray-500">(optional)</span>
<h3 className='mb-4 text-xl font-bold'>
Add-ons{" "}
<span className='text-sm font-normal italic text-gray-500'>
(optional)
</span>
</h3>
<div className="flex gap-3 items-center">
<div className='flex items-center gap-3'>
<button
type="button"
className="mb-2 font-bold text-[#1570EF]"
type='button'
className='mb-2 font-bold text-[#1570EF]'
onClick={() => setAddAddonsPopup(true)}
>
+ Select items
</button>
</div>
<div className="addons-grid mb-12">
<div className='addons-grid mb-12'>
{addons
?.filter((addon) => {
if (Array.isArray(selectedAddons)) {
return selectedAddons?.includes(String(addon.id));
}
return false;
}).sort((a, b) => (a.space_id === null ? -1 : 1) - (b.space_id === null ? -1 : 1))
})
.sort(
(a, b) =>
(a.space_id === null ? -1 : 1) -
(b.space_id === null ? -1 : 1)
)
.map((addon) => (
<li
className="flex w-fit items-center gap-2 mb-4 sm:mb-0"
key={addon.id}>
className='mb-4 flex w-fit items-center gap-2 sm:mb-0'
key={addon.id}
>
<CircleCheckIcon />
{addon.name}</li>
{addon.name}
</li>
))}
</div>
<h3 className="mb-2 text-xl font-bold">
Frequently asked question <span className="text-sm font-normal italic text-gray-500">(optional)</span>
<h3 className='mb-2 text-xl font-bold'>
Frequently asked question{" "}
<span className='text-sm font-normal italic text-gray-500'>
(optional)
</span>
</h3>
<p>These FAQs will show as part of your space listing.</p>
<div>
{fields.map((field, index) => (
<div
className="p-[20px]"
key={field.id}
>
<div className="flex justify-between">
<label className="mb-1 font-semibold">* Question #{index + 1}</label>
<div className='p-[20px]' key={field.id}>
<div className='flex justify-between'>
<label className='mb-1 font-semibold'>
* Question #{index + 1}
</label>
<button
className="text-sm font-semibold text-[#667085]"
className='text-sm font-semibold text-[#667085]'
onClick={() => remove(index)}
>
Delete
</button>
</div>
<input
placeholder=""
autoComplete="off"
placeholder=''
autoComplete='off'
{...register(`faqs.${index}.question`)}
className={`mb-4 w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-primary`}
className={`mb-4 w-full rounded border px-3 py-2 leading-tight text-gray-700 focus:outline-primary`}
/>
<br />
<label className="mb-2 font-semibold">* Answer #{index + 1}</label>
<label className='mb-2 font-semibold'>
* Answer #{index + 1}
</label>
<SunEditor
width="100%"
height="107px"
onChange={(content) => setValue(`faqs.${index}.answer`, content)}
placeholder=""
width='100%'
height='107px'
onChange={(content) =>
setValue(`faqs.${index}.answer`, content)
}
placeholder=''
hideToolbar={true}
setOptions={{ resizingBar: false }}
defaultValue={getValues().faqs[index].answer}
@@ -418,62 +538,76 @@ const SpaceDetailsTwo = () => {
))}
<button
className="mb-12 font-bold text-[#1570EF]"
type="button"
id="append_faq_btn"
// add logic to handle appending new FAQs field
className='mb-12 font-bold text-[#1570EF]'
type='button'
id='append_faq_btn'
onClick={() => append({ question: "", answer: "" })}
>
+ Add question
</button>
</div>
</div>
<hr className="my-[20px]" />
<p className="px-4 text-sm md:px-0" dangerouslySetInnerHTML={{ __html: sanitizeAndTruncate(cancellationPolicy, 400) }}>
</p>
<div className="flex justify-end">
<Link to={"/help/cancellation-policy"}
className="mt-4 text-end text-sm font-semibold underline"
<hr className='my-[20px]' />
<p
className='px-4 text-sm md:px-0'
dangerouslySetInnerHTML={{
__html: sanitizeAndTruncate(cancellationPolicy, 400),
}}
></p>
<div className='flex justify-end'>
<Link
to={"/help/cancellation-policy"}
className='mt-4 text-end text-sm font-semibold underline'
target={"_blank"}
>
View More
</Link>
</div>
<hr className="my-[30px]" />
<hr className='my-[30px]' />
<button
type="submit"
className="login-btn-gradient rounded py-2 px-4 tracking-wide text-white outline-none focus:outline-none"
type='submit'
className='login-btn-gradient rounded px-4 py-2 tracking-wide text-white outline-none focus:outline-none'
>
Continue
</button>
<br />
<div
className={`${showAddAmenitiesPopup ? "flex" : "hidden"} popup-container items-center justify-center normal-case`}
className={`${
showAddAmenitiesPopup ? "flex" : "hidden"
} popup-container items-center justify-center normal-case`}
onClick={() => setAddAmenitiesPopup(false)}
>
<div
className={`${addAmenitiesPopup ? "pop-in" : "pop-out"} w-[510px] max-w-[80%] rounded-lg bg-white p-5 px-3 md:px-5`}
className={`${
addAmenitiesPopup ? "pop-in" : "pop-out"
} w-[510px] max-w-[80%] rounded-lg bg-white p-5 px-3 md:px-5`}
onClick={(e) => e.stopPropagation()}
>
<div className="mb-[18px] flex items-center justify-between">
<h3 className="mb-[8px] text-2xl font-semibold">Select Amenities</h3>
<div className='mb-[18px] flex items-center justify-between'>
<h3 className='mb-[8px] text-2xl font-semibold'>
Select Amenities
</h3>
<button
type="button"
type='button'
onClick={() => setAddAmenitiesPopup(false)}
className="rounded-full border p-1 px-3 text-2xl font-normal duration-100 hover:bg-gray-200 active:bg-gray-300"
className='rounded-full border p-1 px-3 text-2xl font-normal duration-100 hover:bg-gray-200 active:bg-gray-300'
>
&#x2715;
</button>
</div>
<div className="review-scroll max-h-[400px] overflow-y-auto">
{isCatOthers() ?
amenities.sort((a, b) => (a.creator_id !== 1 ? -1 : 1) - (b.creator_id !== 1 ? -1 : 1)).map((am) => (
<div
key={am.id}
className="checkbox-container mb-4"
>
<div className='review-scroll max-h-[400px] overflow-y-auto'>
{isCatOthers()
? amenities
.sort(
(a, b) =>
(a.creator_id !== 1 ? -1 : 1) -
(b.creator_id !== 1 ? -1 : 1)
)
.map((am) => (
<div key={am.id} className='checkbox-container mb-4'>
<input
type="checkbox"
className=""
type='checkbox'
className=''
{...register("amenities")}
id={"amenity" + am.id}
value={am.id}
@@ -481,94 +615,113 @@ const SpaceDetailsTwo = () => {
<label htmlFor={"amenity" + am.id}>{am.name}</label>
</div>
))
:
amenities.filter((am) => (am.space_id === Number(spaceData.category)) || am.creator_id === Number(localStorage.getItem("user"))).sort((a, b) => (a.creator_id !== 1 ? -1 : 1) - (b.creator_id !== 1 ? -1 : 1)).map((am) => (
<div
key={am.id}
className="checkbox-container mb-4"
>
: amenities
.filter(
(am) =>
am.space_id === Number(spaceData.category) ||
am.creator_id === Number(localStorage.getItem("user"))
)
.sort(
(a, b) =>
(a.creator_id !== 1 ? -1 : 1) -
(b.creator_id !== 1 ? -1 : 1)
)
.map((am) => (
<div key={am.id} className='checkbox-container mb-4'>
<input
type="checkbox"
className=""
type='checkbox'
className=''
{...register("amenities")}
id={"amenity" + am.id}
value={am.id}
/>
<label htmlFor={"amenity" + am.id}>{am.name}</label>
</div>
))
}
))}
</div>
</div>
</div>
<div
className={`${showAddAddonsPopup ? "flex" : "hidden"} popup-container items-center justify-center normal-case`}
className={`${
showAddAddonsPopup ? "flex" : "hidden"
} popup-container items-center justify-center normal-case`}
onClick={() => setAddAddonsPopup(false)}
>
<div
className={`${addAddonsPopup ? "pop-in" : "pop-out"} w-[510px] max-w-[80%] rounded-lg bg-white p-5 px-3 md:px-5`}
className={`${
addAddonsPopup ? "pop-in" : "pop-out"
} w-[510px] max-w-[80%] rounded-lg bg-white p-5 px-3 md:px-5`}
onClick={(e) => e.stopPropagation()}
>
<div className="mb-[18px] flex items-center justify-between">
<h3 className="mb-[8px] text-2xl font-semibold">Select Addons</h3>
<div className='mb-[18px] flex items-center justify-between'>
<h3 className='mb-[8px] text-2xl font-semibold'>Select Addons</h3>
<button
type="button"
type='button'
onClick={() => setAddAddonsPopup(false)}
className="rounded-full border p-1 px-3 text-2xl font-normal duration-100 hover:bg-gray-200 active:bg-gray-300"
className='rounded-full border p-1 px-3 text-2xl font-normal duration-100 hover:bg-gray-200 active:bg-gray-300'
>
&#x2715;
</button>
</div>
<div className="review-scroll max-h-[400px] overflow-y-auto">
{isCatOthers() ?
addons.sort((a, b) => (a.creator_id !== 1 ? -1 : 1) - (b.creator_id !== 1 ? -1 : 1)).map((addon) => (
<div
key={addon.id}
className="checkbox-container mb-4"
>
<input
type="checkbox"
{...register("addons")}
id={"addon" + addon.id}
value={addon.id}
/>
<label htmlFor={"addon" + addon.id}>{addon.name}{" "}${addon.cost}</label>
</div>
))
:
addons.sort((a, b) => (a.creator_id !== 1 ? -1 : 1) - (b.creator_id !== 1 ? -1 : 1)).filter((ad) => ad.space_id === Number(spaceData.category) || ad.creator_id === Number(localStorage.getItem("user")))
<div className='review-scroll max-h-[400px] overflow-y-auto'>
{isCatOthers()
? addons
.sort(
(a, b) =>
(a.creator_id !== 1 ? -1 : 1) -
(b.creator_id !== 1 ? -1 : 1)
)
.map((addon) => (
<div
key={addon.id}
className="checkbox-container mb-4"
>
<div key={addon.id} className='checkbox-container mb-4'>
<input
type="checkbox"
type='checkbox'
{...register("addons")}
id={"addon" + addon.id}
value={addon.id}
/>
<label htmlFor={"addon" + addon.id}>{addon.name}{" "}${addon.cost}</label>
<label htmlFor={"addon" + addon.id}>
{addon.name} ${addon.cost}
</label>
</div>
))
}
: addons
.sort(
(a, b) =>
(a.creator_id !== 1 ? -1 : 1) -
(b.creator_id !== 1 ? -1 : 1)
)
.filter(
(ad) =>
ad.space_id === Number(spaceData.category) ||
ad.creator_id === Number(localStorage.getItem("user"))
)
.map((addon) => (
<div key={addon.id} className='checkbox-container mb-4'>
<input
type='checkbox'
{...register("addons")}
id={"addon" + addon.id}
value={addon.id}
/>
<label htmlFor={"addon" + addon.id}>
{addon.name} ${addon.cost}
</label>
</div>
))}
</div>
</div>
</div>
<button
type="button"
id="save-as-draft"
className="mt-[24px] rounded border-2 border-[#98A2B3] py-2 px-4 tracking-wide outline-none focus:outline-none"
type='button'
id='save-as-draft'
className='mt-[24px] rounded border-2 border-[#98A2B3] px-4 py-2 tracking-wide outline-none focus:outline-none'
onClick={() => onSaveDraft()}
>
Save draft and exit
</button>
</form>
{addOnModal &&
<HostAddAddonsModal setAddOnModal={setAddOnModal}/>
}
{addOnModal && <HostAddAddonsModal setAddOnModal={setAddOnModal} />}
</div>
);
};