sides edit module updated and menuitem create and update flow set

This commit is contained in:
alaguraj 2025-07-26 13:46:41 +05:30
parent a75de8c589
commit f9f29e885e
3 changed files with 212 additions and 250 deletions

View File

@ -386,7 +386,7 @@ const ProductListInner = () => {
className="dropdown-item px-16 py-8 d-flex align-items-center gap-2 rounded text-secondary-light bg-hover-neutral-200 text-hover-neutral-900" className="dropdown-item px-16 py-8 d-flex align-items-center gap-2 rounded text-secondary-light bg-hover-neutral-200 text-hover-neutral-900"
onClick={(e) => { onClick={(e) => {
e.preventDefault(); e.preventDefault();
router.push(`/admin/pos/update-product?menuitemname=${menu.name}`) router.push(`/admin/pos/update-product?category=${activeCategory}&menuitemname=${menu.name}`)
// handleFloorEdit(menu); // handleFloorEdit(menu);
}} }}
> >

View File

@ -1,20 +1,22 @@
"use client"; "use client";
import { useState } from "react"; import { useEffect, useState } from "react";
import "highlight.js/styles/github.css"; import "highlight.js/styles/github.css";
import client from "@auth"; import client from "@auth";
import { Icon } from "@iconify/react"; import { Icon } from "@iconify/react";
import MasterLayout from "@/masterLayout/MasterLayout"; import MasterLayout from "@/masterLayout/MasterLayout";
import Breadcrumb from "@/components/Breadcrumb"; import Breadcrumb from "@/components/Breadcrumb";
import { useSearchParams } from "next/navigation"; import { useSearchParams } from "next/navigation";
import axios from "axios";
import { Baseurl } from "@utils/BaseUrl.utils";
const UpdateProduct = () => { const UpdateProduct = () => {
const searchParams = useSearchParams();
const searchParams = useSearchParams() const category = searchParams.get("category");
const category = searchParams.get('category') const MenuItemName = searchParams.get("menuitemname");
console.log("category", category)
const [imagePreview, setImagePreview] = useState(null); const [imagePreview, setImagePreview] = useState(null);
const [imageFile, setImageFile] = useState(null); const [imageFile, setImageFile] = useState(null);
const [errors, setErrors] = useState({});
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
menuitemname: "", menuitemname: "",
@ -26,7 +28,33 @@ const UpdateProduct = () => {
description: "", description: "",
}); });
const [errors, setErrors] = useState({}); useEffect(() => {
if (category) {
getMenuFoodItems();
}
}, [category]);
const getMenuFoodItems = async () => {
try {
const res = await client.get(
`/Dine360%20Menu%20Category/${category}?fields=["*"]&limit_page_length=100`
);
const item = res?.data?.data?.menuitems_child?.[0];
if (item) {
setFormData((prev) => ({
...prev,
...item,
is_active: !!item.is_active,
is_special: !!item.is_special,
}));
if (item.image_item) {
setImagePreview(`${process.env.NEXT_PUBLIC_IMAGE_BASE}/${item.image_item}`);
}
}
} catch (error) {
console.error("Error fetching menu items:", error);
}
};
const handleChange = (e) => { const handleChange = (e) => {
const { name, value, type, checked } = e.target; const { name, value, type, checked } = e.target;
@ -66,7 +94,7 @@ const UpdateProduct = () => {
if (!formData.description || formData.description.trim().length < 5) { if (!formData.description || formData.description.trim().length < 5) {
newErrors.description = "Description is too short"; newErrors.description = "Description is too short";
} }
if (!imageFile) { if (!imageFile && !imagePreview) {
newErrors.image_item = "Image is required"; newErrors.image_item = "Image is required";
} }
return newErrors; return newErrors;
@ -81,19 +109,6 @@ const UpdateProduct = () => {
} }
setErrors({}); setErrors({});
const data = new FormData();
data.append("menuitemname", formData.menuitemname);
data.append("price", formData.price.toString());
data.append("is_active", formData.is_active ? "1" : "0");
data.append("is_special", formData.is_special ? "1" : "0");
data.append("availability_time", formData.availability_time);
data.append("preparation_time", formData.preparation_time.toString());
data.append("description", formData.description);
if (imageFile) data.append("image_item", imageFile);
// for (let pair of data.entries()) {
// console.log(pair[0], pair[1]);
// }
const body = { const body = {
menucategoryname: category, menucategoryname: category,
description: formData.description, description: formData.description,
@ -107,41 +122,47 @@ const UpdateProduct = () => {
is_special: formData.is_special ? 1 : 0, is_special: formData.is_special ? 1 : 0,
availability_time: formData.availability_time, availability_time: formData.availability_time,
preparation_time: formData.preparation_time, preparation_time: formData.preparation_time,
} },
] ],
} };
try { try {
const res = await client.post(`/Dine360%20Menu%20Category/${category}`, body); const formDataToSend = new FormData();
console.log('res', res) formDataToSend.append("endpoint", `Dine360%20Menu%20Category/${category}`);
formDataToSend.append("body", JSON.stringify(body));
formDataToSend.append("file", imageFile); // use imageFile
formDataToSend.append("fileid", "image_item");
formDataToSend.append("data", JSON.stringify({ name: MenuItemName }));
const response = await axios.put(`${Baseurl}/Upload-Image-To-Frappe`, formDataToSend, {
headers: {
Authorization: "token 482beca79d9c005:b8778f51fcca82b",
},
});
console.log("response", response);
alert("Form submitted successfully!"); alert("Form submitted successfully!");
router.push("/admin/pos/product-list");
} catch (error) {
console.log("error", error);
} }
catch (error) {
console.log("error", error)
}
}; };
return ( return (
<MasterLayout> <MasterLayout>
{/* Breadcrumb */} <Breadcrumb title="Create Product" />
<Breadcrumb title='Create Product' />
<div className="container" style={{ marginBottom: "100px" }}> <div className="container" style={{ marginBottom: "100px" }}>
<div className='row gy-4 d-flex justify-content-center'> <div className="row gy-4 d-flex justify-content-center">
<div className='col-xxl-6 col-xl-6 col-lg-7 col-md-9'> <div className="col-xxl-6 col-xl-6 col-lg-7 col-md-9">
<div className='card mt-24 p-lg-3'> <div className="card mt-24 p-lg-3">
<div className="card-body">
<div className='card-body'> <h6 className="text-xl mb-3">Add New Product</h6>
<h6 className='text-xl mb-3'>Add New Product</h6> <form
<form onSubmit={handleSubmit} className='d-flex flex-column gap-20'> onSubmit={handleSubmit}
className="d-flex flex-column gap-20"
>
<div className="row"> <div className="row">
<div className="col-xl-6"> <div className="col-xl-6">
<label <label className="form-label fw-bold text-neutral-900">
className='form-label fw-bold text-neutral-900'
htmlFor='title'
>
Menu Item Name Menu Item Name
</label> </label>
<input <input
@ -149,47 +170,55 @@ const UpdateProduct = () => {
name="menuitemname" name="menuitemname"
value={formData.menuitemname} value={formData.menuitemname}
onChange={handleChange} onChange={handleChange}
className="form-control border border-neutral-200 radius-8" className="form-control border border-neutral-200 radius-8"
placeholder="Enter name" placeholder="Enter name"
/> />
{errors.menuitemname && ( {errors.menuitemname && (
<div className="text-danger small">{errors.menuitemname}</div> <div className="text-danger small">
{errors.menuitemname}
</div>
)} )}
</div> </div>
<div className="col-xl-6"> <div className="col-xl-6">
<label className="form-label fw-bold text-neutral-900">Price ($)</label> <label className="form-label fw-bold text-neutral-900">
Price ($)
</label>
<input <input
type="number" type="number"
name="price" name="price"
value={formData.price} value={formData.price}
onChange={handleChange} onChange={handleChange}
className="form-control border border-neutral-200 radius-8 " className="form-control border border-neutral-200 radius-8"
/> />
{errors.price && <div className="text-danger small">{errors.price}</div>} {errors.price && (
<div className="text-danger small">
{errors.price}
</div>
)}
</div> </div>
</div> </div>
<div> <div>
<label className="form-label fw-bold text-neutral-900">Description</label> <label className="form-label fw-bold text-neutral-900">
Description
</label>
<textarea <textarea
name="description" name="description"
rows={4} rows={4}
className="form-control border border-neutral-200 radius-8" className="form-control border border-neutral-200 radius-8"
placeholder="Enter description..." placeholder="Enter description..."
value={formData.description} value={formData.description}
onChange={handleChange} onChange={handleChange}
></textarea> ></textarea>
{errors.description && ( {errors.description && (
<div className="text-danger small">{errors.description}</div> <div className="text-danger small">
{errors.description}
</div>
)} )}
</div> </div>
<div className="row"> <div className="row">
{/* Is Active */} <div className="form-check col-6 d-flex justify-content-start gap-2 align-items-center">
<div className="form-check col-6 d-flex justify-content-start gap-2 align-items-center">
<input <input
type="checkbox" type="checkbox"
name="is_active" name="is_active"
@ -198,13 +227,14 @@ const UpdateProduct = () => {
onChange={handleChange} onChange={handleChange}
id="is_active" id="is_active"
/> />
<label className="form-check-label" htmlFor="is_active"> <label
className="form-check-label"
htmlFor="is_active"
>
Is Active Is Active
</label> </label>
</div> </div>
<div className="form-check col-6 d-flex justify-content-start gap-2 align-items-center">
{/* Is Special */}
<div className="form-check col-6 d-flex justify-content-start gap-2 align-items-center ">
<input <input
type="checkbox" type="checkbox"
name="is_special" name="is_special"
@ -213,7 +243,10 @@ const UpdateProduct = () => {
onChange={handleChange} onChange={handleChange}
id="is_special" id="is_special"
/> />
<label className="form-check-label" htmlFor="is_special"> <label
className="form-check-label"
htmlFor="is_special"
>
Is Special Is Special
</label> </label>
</div> </div>
@ -221,75 +254,76 @@ const UpdateProduct = () => {
<div className="row"> <div className="row">
<div className="col-xl-6"> <div className="col-xl-6">
<label className="form-label fw-bold text-neutral-900">Availability Time</label> <label className="form-label fw-bold text-neutral-900">
Availability Time
</label>
<input <input
type="time" type="time"
name="availability_time" name="availability_time"
value={formData.availability_time} value={formData.availability_time}
onChange={handleChange} onChange={handleChange}
className="form-control border border-neutral-200 radius-8" className="form-control border border-neutral-200 radius-8"
/> />
{errors.availability_time && ( {errors.availability_time && (
<div className="text-danger small">{errors.availability_time}</div> <div className="text-danger small">
{errors.availability_time}
</div>
)} )}
</div> </div>
<div className="col-xl-6"> <div className="col-xl-6">
<label className="form-label fw-bold text-neutral-900">Preparation Time (minutes)</label> <label className="form-label fw-bold text-neutral-900">
Preparation Time (minutes)
</label>
<input <input
type="number" type="number"
name="preparation_time" name="preparation_time"
value={formData.preparation_time} value={formData.preparation_time}
onChange={handleChange} onChange={handleChange}
className="form-control border border-neutral-200 radius-8 " className="form-control border border-neutral-200 radius-8"
/> />
{errors.preparation_time && ( {errors.preparation_time && (
<div className="text-danger small">{errors.preparation_time}</div> <div className="text-danger small">
{errors.preparation_time}
</div>
)} )}
</div> </div>
</div> </div>
<div> <div>
<label className='form-label fw-bold text-neutral-900'> <label className="form-label fw-bold text-neutral-900">
Menu Item Image Menu Item Image
</label> </label>
<div className='upload-image-wrapper'> <div className="upload-image-wrapper">
{imagePreview ? ( {imagePreview ? (
<div className='uploaded-img position-relative h-160-px w-100 border input-form-light radius-8 overflow-hidden border-dashed bg-neutral-50'> <div className="uploaded-img position-relative h-160-px w-100 border input-form-light radius-8 overflow-hidden border-dashed bg-neutral-50">
<button <button
type='button' type="button"
onClick={handleRemoveImage} onClick={handleRemoveImage}
className='uploaded-img__remove position-absolute top-0 end-0 z-1 text-2xxl line-height-1 me-8 mt-8 d-flex' className="uploaded-img__remove position-absolute top-0 end-0 z-1 text-2xxl line-height-1 me-8 mt-8 d-flex"
aria-label='Remove uploaded image' aria-label="Remove uploaded image"
> >
<Icon <Icon
icon='radix-icons:cross-2' icon="radix-icons:cross-2"
className='text-xl text-danger-600' className="text-xl text-danger-600"
></Icon> />
</button> </button>
<img <img
id='uploaded-img__preview' id="uploaded-img__preview"
className='w-100 h-100 object-fit-cover' className="w-100 h-100 object-fit-cover"
src={imagePreview} src={imagePreview}
alt='Uploaded' alt="Uploaded"
/> />
</div> </div>
) : ( ) : (
<label <label
className='upload-file h-160-px w-100 border input-form-light radius-8 overflow-hidden border-dashed bg-neutral-50 bg-hover-neutral-200 d-flex align-items-center flex-column justify-content-center gap-1' className="upload-file h-160-px w-100 border input-form-light radius-8 overflow-hidden border-dashed bg-neutral-50 bg-hover-neutral-200 d-flex align-items-center flex-column justify-content-center gap-1"
htmlFor='upload-file' htmlFor="upload-file"
> >
<iconify-icon <Icon icon="solar:camera-outline" className="text-xl text-secondary-light" />
icon='solar:camera-outline' <span className="fw-semibold text-secondary-light">Upload</span>
className='text-xl text-secondary-light'
></iconify-icon>
<span className='fw-semibold text-secondary-light'>
Upload
</span>
<input <input
id='upload-file' id="upload-file"
type='file' type="file"
hidden hidden
onChange={handleFileChange} onChange={handleFileChange}
/> />
@ -300,14 +334,15 @@ const UpdateProduct = () => {
)} )}
</div> </div>
</div> </div>
<div className="row"> <div className="row">
<div className="col-6"> <div className="col-6">
<button type='submit' className='btn btn-outline-theme radius-8' style={{ width: "100%" }}> <button type="reset" className="btn btn-outline-theme radius-8 w-100">
Cancel Cancel
</button> </button>
</div> </div>
<div className="col-6"> <div className="col-6">
<button type='submit' className='btn btn-bg-theme radius-8' style={{ width: "100%" }}> <button type="submit" className="btn btn-bg-theme radius-8 w-100">
Submit Submit
</button> </button>
</div> </div>
@ -316,7 +351,6 @@ const UpdateProduct = () => {
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</MasterLayout> </MasterLayout>

View File

@ -28,6 +28,7 @@ const SidesPageInner = () => {
description: "", description: "",
price: "", price: "",
item_image: null, item_image: null,
image_preview: null,
}); });
const [errors, setErrors] = useState({}); const [errors, setErrors] = useState({});
const [deleteConfirm, setDeleteConfirm] = useState({ show: false, id: null }); const [deleteConfirm, setDeleteConfirm] = useState({ show: false, id: null });
@ -75,15 +76,12 @@ const SidesPageInner = () => {
const handleSubmit = async (e) => { const handleSubmit = async (e) => {
e.preventDefault(); e.preventDefault();
const newErrors = {}; const newErrors = {};
if (!formData.sidename.trim()) newErrors.sidename = "Side Name is required"; if (!formData.sidename.trim()) newErrors.sidename = "Side Name is required";
if (!formData.description.trim()) newErrors.description = "Description is required"; if (!formData.description.trim()) newErrors.description = "Description is required";
if (!formData.price) newErrors.price = "Price is required"; if (!formData.price) newErrors.price = "Price is required";
if (!editMode && !formData.item_image) { if (!editMode && !formData.item_image) {
newErrors.item_image = "Item image is required"; newErrors.item_image = "Item image is required";
} }
setErrors(newErrors); setErrors(newErrors);
if (Object.keys(newErrors).length > 0) return; if (Object.keys(newErrors).length > 0) return;
@ -96,50 +94,27 @@ const SidesPageInner = () => {
}; };
try { try {
let itemId; const formDataToSend = new FormData();
formDataToSend.append("endpoint", "Dine360 Food Sides");
formDataToSend.append("body", JSON.stringify(body));
if (formData.item_image instanceof File) {
formDataToSend.append("file", formData.item_image);
formDataToSend.append("fileid", "item_image");
}
if (editMode) { if (editMode) {
itemId = editingRoomId; formDataToSend.append("data", JSON.stringify({ name: editingRoomId }));
await client.put(`${Baseurl}/Upload-Image-To-Frappe`, formDataToSend, {
// Step 2: Upload new image if updated
if (formData.item_image?.startsWith("data:image")) {
const uploadPath = await uploadImage(formData.item_image, "Dine360 Food Sides", itemId);
body.item_image = uploadPath;
}
// Step 3: Update
await client.put(`/Dine360%20Food%20Sides/${itemId}`, body);
} else {
// Step 1: Create new item with image
const formDataToSend = new FormData();
formDataToSend.append("endpoint", "Dine360 Food Sides");
formDataToSend.append("body", JSON.stringify(body));
// Append image file (should be File object, not base64)
formDataToSend.append("file", formData.item_image); // `imageFile` should be a real File from <input type="file" />
formDataToSend.append("fileid", "item_image");
const response = await axios.post(`${Baseurl}/Upload-Image-To-Frappe`, formDataToSend, {
headers: { headers: {
"Authorization": "token 482beca79d9c005:b8778f51fcca82b", Authorization: "token 482beca79d9c005:b8778f51fcca82b",
},
});
} else {
await axios.post(`${Baseurl}/Upload-Image-To-Frappe`, formDataToSend, {
headers: {
Authorization: "token 482beca79d9c005:b8778f51fcca82b",
}, },
}); });
// Handle response if needed
console.log("Upload success:", response.data);
// // Step 1: Create without image
// const res = await client.post(`/Dine360%20Food%20Sides`, body);
// itemId = res?.data?.data?.name;
// // Step 2: Upload image
// if (formData.item_image) {
// const uploadPath = await uploadImage(formData.item_image, "Dine360 Food Sides", itemId);
// // Step 3: Update with image
// await client.put(`/Dine360%20Food%20Sides/${itemId}`, { item_image: uploadPath });
// }
} }
getSideData(); getSideData();
@ -147,56 +122,45 @@ const SidesPageInner = () => {
} catch (error) { } catch (error) {
console.error("❌ Submission error:", error); console.error("❌ Submission error:", error);
const backendError = error?.response?.data?.exception || ""; const backendError = error?.response?.data?.exception || "";
if (backendError.includes("Data too long for column 'item_image'")) { if (backendError.includes("Data too long for column 'item_image'")) {
setErrors((prev) => ({ setErrors((prev) => ({
...prev, ...prev,
item_image: "Data too long for column 'item_image'", item_image: "Data too long for column 'item_image'",
})); }));
} else if (error?.response?.status === 500) {
alert("Server error occurred. Please try again later.");
} else { } else {
alert("Submission failed. Please try again."); alert("Submission failed. Please try again.");
} }
} }
}; };
const uploadImage = async (base64Data, doctype, docname) => {
const blob = await (await fetch(base64Data)).blob();
const form = new FormData();
form.append("file", blob, "side.jpg");
form.append("is_private", "0");
form.append("doctype", doctype);
form.append("docname", docname);
const res = await fetch(`${ImageBase}/api/method/upload_file`, {
method: "POST",
headers: {
"Authorization": "token 482beca79d9c005:b8778f51fcca82b",
// Don't set Content-Type when using FormData let the browser set it
},
body: form,
});
const result = await res.json();
if (!result.message?.file_url) throw new Error("Upload failed");
return result.message.file_url;
};
const handleEdit = (room) => { const handleEdit = (room) => {
setFormData({ setFormData({
sidename: room.sidename || "", sidename: room.sidename || "",
description: room.description || "", description: room.description || "",
price: room.price || "", price: room.price || "",
item_image: room.item_image || null, item_image: room.item_image || null,
image_preview: null,
}); });
setEditingRoomId(room.name); setEditingRoomId(room.name);
setEditMode(true); setEditMode(true);
setShowModal(true); setShowModal(true);
}; };
const resetForm = () => {
if (formData.image_preview) URL.revokeObjectURL(formData.image_preview);
setFormData({
sidename: "",
description: "",
price: "",
item_image: null,
image_preview: null,
});
setErrors({});
setEditMode(false);
setEditingRoomId(null);
setShowModal(false);
};
const handleDelete = async () => { const handleDelete = async () => {
try { try {
await client.delete(`/Dine360%20Food%20Sides/${deleteConfirm?.id}`); await client.delete(`/Dine360%20Food%20Sides/${deleteConfirm?.id}`);
@ -207,32 +171,15 @@ const SidesPageInner = () => {
} }
}; };
const resetForm = () => {
setFormData({
sidename: "",
description: "",
price: "",
item_image: null,
});
setErrors({});
setEditMode(false);
setEditingRoomId(null);
setShowModal(false);
};
return ( return (
<div className="container-fluid" style={{ marginBottom: "100px" }}> <div className="container-fluid" style={{ marginBottom: "100px" }}>
{/* Header + Create Button */}
<div className=""> <div className="">
<div className=""> <div className="">
<div className="d-flex justify-content-between align-items-center mb-3"> <div className="d-flex justify-content-between align-items-center mb-3">
<h6 className="mb-0">Sides</h6> <h6 className="mb-0">Sides</h6>
<button className="btn btn-bg-theme radius-8 px-20 py-11" onClick={() => setShowModal(true)}> <button className="btn btn-bg-theme radius-8 px-20 py-11" onClick={() => setShowModal(true)}>Create</button>
Create
</button>
</div> </div>
{/* Grid */}
<div className="row gy-3 gx-3 gx-lg-5 gy-lg-5 justify-content-center"> <div className="row gy-3 gx-3 gx-lg-5 gy-lg-5 justify-content-center">
{loading ? ( {loading ? (
<PageLoader /> <PageLoader />
@ -243,65 +190,31 @@ const SidesPageInner = () => {
const gradientClass = gradientClasses[index % gradientClasses.length]; const gradientClass = gradientClasses[index % gradientClasses.length];
return ( return (
<div className="col-xxl-2 col-lg-4 col-sm-6 cursor-pointer" key={room?.name}> <div className="col-xxl-2 col-lg-4 col-sm-6 cursor-pointer" key={room?.name}>
<div className={`card p-3 shadow-2 radius-8 h-100 border border-white position-relative`}> <div className="card p-3 shadow-2 radius-8 h-100 border border-white position-relative">
<div className="position-absolute top-0 end-0 me-1 mt-1 d-flex gap-2"> <div className="position-absolute top-0 end-0 me-1 mt-1 d-flex gap-2">
<div className="dropdown"> <div className="dropdown">
<div className='dropdown'> <button className='btn px-1 py-1 d-flex align-items-center text-primary-light' type='button' data-bs-toggle='dropdown'>
<button <Icon icon='entypo:dots-three-vertical' className='menu-icon' />
className='btn px-1 py-1 d-flex align-items-center text-primary-light' </button>
type='button' <ul className='dropdown-menu'>
data-bs-toggle='dropdown' <li>
aria-expanded='false' <Link href="#" className='dropdown-item' onClick={(e) => { e.preventDefault(); handleEdit(room); }}>
> <Icon icon='lucide:edit' className='menu-icon' /> Edit
<Icon icon='entypo:dots-three-vertical' className='menu-icon' /> </Link>
</button> </li>
<ul className='dropdown-menu'> <li>
<li> <Link href="#" className='dropdown-item' onClick={(e) => { e.preventDefault(); setDeleteConfirm({ show: true, id: room.name }); }}>
<Link <Icon icon='fluent:delete-24-regular' className='menu-icon' /> Delete
href="#" </Link>
className='dropdown-item px-16 py-8 d-flex align-items-center gap-2 rounded text-secondary-light bg-hover-neutral-200 text-hover-neutral-900' </li>
onClick={(e) => { </ul>
e.preventDefault();
handleEdit(room);
}}
> <Icon icon='lucide:edit' className='menu-icon' />
Edit
</Link>
</li>
<li>
<Link
href="#"
className='dropdown-item px-16 py-8 d-flex align-items-center gap-2 rounded text-secondary-light bg-hover-neutral-200 text-hover-neutral-900'
onClick={(e) => {
e.preventDefault();
setDeleteConfirm({ show: true, id: room.name });
}}
>
<Icon
icon='fluent:delete-24-regular'
className='menu-icon'
/>
Delete
</Link>
</li>
</ul>
</div>
</div> </div>
</div> </div>
<div className="card-body text-center p-3"> <div className="card-body text-center p-3">
<img <img src={`${ImageBase}/${room.item_image}`} alt="" className="w-100-px h-100-px rounded-circle object-fit-cover" />
src={`${ImageBase}/${room.item_image}`} <h6 className="text-lg mb-0 mt-1">{room?.sidename}</h6>
alt="" <span className="text-secondary-light text-sm">{room?.description}</span>
className="border br-white border-width-2-px w-100-px h-100-px rounded-circle object-fit-cover" <h6 className="text-md mb-0 mt-1">${room.price.toFixed(2)}</h6>
/>
<div className="p-0">
<h6 className="text-lg mb-0 mt-1">{room?.sidename}</h6>
<span className="text-secondary-light text-sm lh-sm mb-1">{room?.description}</span>
<h6 className="text-md mb-0 mt-1">${room.price.toFixed(2)}</h6>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -318,12 +231,11 @@ const SidesPageInner = () => {
<div className="modal-dialog modal-dialog-centered"> <div className="modal-dialog modal-dialog-centered">
<div className="modal-content"> <div className="modal-content">
<div className="modal-header border-0 pb-0"> <div className="modal-header border-0 pb-0">
<h6 className="modal-title text-lg">{editMode ? "Edit Side" : "Create Side"}</h6> <h6 className="modal-title">{editMode ? "Edit Side" : "Create Side"}</h6>
<button type="button" className="btn-close" onClick={resetForm}></button> <button type="button" className="btn-close" onClick={resetForm}></button>
</div> </div>
<div className="modal-body"> <div className="modal-body">
<form onSubmit={handleSubmit}> <form onSubmit={handleSubmit}>
{/* Form fields */}
<div className="mb-3"> <div className="mb-3">
<label className="form-label">Side Name</label> <label className="form-label">Side Name</label>
<input type="text" className={`form-control ${errors.sidename ? "is-invalid" : ""}`} name="sidename" value={formData.sidename} onChange={handleChange} /> <input type="text" className={`form-control ${errors.sidename ? "is-invalid" : ""}`} name="sidename" value={formData.sidename} onChange={handleChange} />
@ -345,38 +257,55 @@ const SidesPageInner = () => {
<div className="mb-3"> <div className="mb-3">
<label className="form-label">Item Image</label> <label className="form-label">Item Image</label>
<div className="d-flex align-items-center gap-3"> <div className="d-flex align-items-center gap-3">
<input type="file" accept="image/*" className={`form-control ${errors.item_image ? "is-invalid" : ""}`} <input
style={{ maxWidth: "350px" }} type="file"
accept="image/*"
className={`form-control ${errors.item_image ? "is-invalid" : ""}`}
onChange={(e) => { onChange={(e) => {
const file = e.target.files[0]; const file = e.target.files[0];
if (!file) return; if (!file) return;
if (!file.type.startsWith("image/")) { if (!file.type.startsWith("image/")) {
setErrors(prev => ({ ...prev, item_image: "Only image files are allowed." })); setErrors(prev => ({ ...prev, item_image: "Only image files allowed." }));
return; return;
} }
if (file.size / (1024 * 1024) > 1) { if (file.size / 1024 / 1024 > 1) {
setErrors(prev => ({ ...prev, item_image: "Max size 1MB" })); setErrors(prev => ({ ...prev, item_image: "Max size 1MB" }));
return; return;
} }
setFormData(prev => ({ ...prev, item_image: file })); // Save raw File const previewURL = URL.createObjectURL(file);
setFormData(prev => ({
...prev,
item_image: file,
image_preview: previewURL,
}));
setErrors(prev => ({ ...prev, item_image: null })); setErrors(prev => ({ ...prev, item_image: null }));
}} }}
/> />
{formData.item_image && ( {(formData.image_preview || formData.item_image) && (
<div style={{ position: "relative", width: "60px", height: "60px" }}> <div style={{ position: "relative", width: "60px", height: "60px" }}>
<img <img
src={URL.createObjectURL(formData.item_image)} src={
formData.image_preview
? formData.image_preview
: `${ImageBase}/${formData.item_image}`
}
alt="preview" alt="preview"
className="img-thumbnail" className="img-thumbnail"
style={{ objectFit: "cover" }} style={{ objectFit: "cover", width: "100%", height: "100%" }}
/> />
<button <button
type="button" type="button"
className="btn btn-sm btn-danger position-absolute top-0 end-0 p-1 pt-0 pb-0" className="btn btn-sm btn-danger position-absolute top-0 end-0 p-1 pt-0 pb-0"
onClick={() => setFormData(prev => ({ ...prev, item_image: null }))} onClick={() =>
setFormData(prev => ({
...prev,
item_image: null,
image_preview: null,
}))
}
> >
× ×
</button> </button>
@ -419,7 +348,6 @@ const SidesPageInner = () => {
</div> </div>
); );
}; };
const SidesPage = () => ( const SidesPage = () => (
<MasterLayout> <MasterLayout>
<Breadcrumb title="Sides Management" /> <Breadcrumb title="Sides Management" />