Removed tags and added cards
This commit is contained in:
@@ -15,10 +15,6 @@ const Item = sequelize.define('Item', {
|
|||||||
type: DataTypes.TEXT,
|
type: DataTypes.TEXT,
|
||||||
allowNull: false
|
allowNull: false
|
||||||
},
|
},
|
||||||
tags: {
|
|
||||||
type: DataTypes.ARRAY(DataTypes.STRING),
|
|
||||||
defaultValue: []
|
|
||||||
},
|
|
||||||
pickUpAvailable: {
|
pickUpAvailable: {
|
||||||
type: DataTypes.BOOLEAN,
|
type: DataTypes.BOOLEAN,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ const router = express.Router();
|
|||||||
router.get('/', async (req, res) => {
|
router.get('/', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const {
|
const {
|
||||||
tags,
|
|
||||||
isPortable,
|
isPortable,
|
||||||
minPrice,
|
minPrice,
|
||||||
maxPrice,
|
maxPrice,
|
||||||
@@ -19,10 +18,6 @@ router.get('/', async (req, res) => {
|
|||||||
|
|
||||||
const where = {};
|
const where = {};
|
||||||
|
|
||||||
if (tags) {
|
|
||||||
const tagsArray = Array.isArray(tags) ? tags : [tags];
|
|
||||||
where.tags = { [Op.overlap]: tagsArray };
|
|
||||||
}
|
|
||||||
if (isPortable !== undefined) where.isPortable = isPortable === 'true';
|
if (isPortable !== undefined) where.isPortable = isPortable === 'true';
|
||||||
if (minPrice || maxPrice) {
|
if (minPrice || maxPrice) {
|
||||||
where.pricePerDay = {};
|
where.pricePerDay = {};
|
||||||
@@ -65,14 +60,9 @@ router.get('/recommendations', authenticateToken, async (req, res) => {
|
|||||||
include: [{ model: Item, as: 'item' }]
|
include: [{ model: Item, as: 'item' }]
|
||||||
});
|
});
|
||||||
|
|
||||||
const rentedTags = userRentals.reduce((tags, rental) => {
|
// For now, just return random available items as recommendations
|
||||||
return [...tags, ...(rental.item.tags || [])];
|
|
||||||
}, []);
|
|
||||||
const uniqueTags = [...new Set(rentedTags)];
|
|
||||||
|
|
||||||
const recommendations = await Item.findAll({
|
const recommendations = await Item.findAll({
|
||||||
where: {
|
where: {
|
||||||
tags: { [Op.overlap]: uniqueTags },
|
|
||||||
availability: true
|
availability: true
|
||||||
},
|
},
|
||||||
limit: 10,
|
limit: 10,
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import AvailabilityCalendar from "../components/AvailabilityCalendar";
|
|||||||
interface ItemFormData {
|
interface ItemFormData {
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
tags: string[];
|
|
||||||
pickUpAvailable: boolean;
|
pickUpAvailable: boolean;
|
||||||
localDeliveryAvailable: boolean;
|
localDeliveryAvailable: boolean;
|
||||||
localDeliveryRadius?: number;
|
localDeliveryRadius?: number;
|
||||||
@@ -45,7 +44,6 @@ const CreateItem: React.FC = () => {
|
|||||||
const [formData, setFormData] = useState<ItemFormData>({
|
const [formData, setFormData] = useState<ItemFormData>({
|
||||||
name: "",
|
name: "",
|
||||||
description: "",
|
description: "",
|
||||||
tags: [],
|
|
||||||
pickUpAvailable: false,
|
pickUpAvailable: false,
|
||||||
localDeliveryAvailable: false,
|
localDeliveryAvailable: false,
|
||||||
localDeliveryRadius: 25,
|
localDeliveryRadius: 25,
|
||||||
@@ -64,7 +62,6 @@ const CreateItem: React.FC = () => {
|
|||||||
needsTraining: false,
|
needsTraining: false,
|
||||||
unavailablePeriods: [],
|
unavailablePeriods: [],
|
||||||
});
|
});
|
||||||
const [tagInput, setTagInput] = useState("");
|
|
||||||
const [imageFiles, setImageFiles] = useState<File[]>([]);
|
const [imageFiles, setImageFiles] = useState<File[]>([]);
|
||||||
const [imagePreviews, setImagePreviews] = useState<string[]>([]);
|
const [imagePreviews, setImagePreviews] = useState<string[]>([]);
|
||||||
const [priceType, setPriceType] = useState<"hour" | "day">("day");
|
const [priceType, setPriceType] = useState<"hour" | "day">("day");
|
||||||
@@ -91,10 +88,10 @@ const CreateItem: React.FC = () => {
|
|||||||
formData.city,
|
formData.city,
|
||||||
formData.state,
|
formData.state,
|
||||||
formData.zipCode,
|
formData.zipCode,
|
||||||
formData.country
|
formData.country,
|
||||||
].filter(part => part && part.trim());
|
].filter((part) => part && part.trim());
|
||||||
|
|
||||||
const location = locationParts.join(', ');
|
const location = locationParts.join(", ");
|
||||||
|
|
||||||
const response = await api.post("/items", {
|
const response = await api.post("/items", {
|
||||||
...formData,
|
...formData,
|
||||||
@@ -129,23 +126,6 @@ const CreateItem: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const addTag = () => {
|
|
||||||
if (tagInput.trim() && !formData.tags.includes(tagInput.trim())) {
|
|
||||||
setFormData((prev) => ({
|
|
||||||
...prev,
|
|
||||||
tags: [...prev.tags, tagInput.trim()],
|
|
||||||
}));
|
|
||||||
setTagInput("");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const removeTag = (tag: string) => {
|
|
||||||
setFormData((prev) => ({
|
|
||||||
...prev,
|
|
||||||
tags: prev.tags.filter((t) => t !== tag),
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleImageChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleImageChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const files = Array.from(e.target.files || []);
|
const files = Array.from(e.target.files || []);
|
||||||
|
|
||||||
@@ -186,429 +166,430 @@ const CreateItem: React.FC = () => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit}>
|
||||||
<div className="mb-3">
|
{/* Images Card */}
|
||||||
<label className="form-label">Images (Max 5)</label>
|
<div className="card mb-4">
|
||||||
<input
|
<div className="card-body">
|
||||||
type="file"
|
<div className="mb-3">
|
||||||
className="form-control"
|
<label className="form-label">Upload Images (Max 5)</label>
|
||||||
onChange={handleImageChange}
|
<input
|
||||||
accept="image/*"
|
type="file"
|
||||||
multiple
|
className="form-control"
|
||||||
disabled={imageFiles.length >= 5}
|
onChange={handleImageChange}
|
||||||
/>
|
accept="image/*"
|
||||||
<div className="form-text">
|
multiple
|
||||||
Upload up to 5 images of your item
|
disabled={imageFiles.length >= 5}
|
||||||
</div>
|
/>
|
||||||
|
<div className="form-text">
|
||||||
|
Upload up to 5 images of your item
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{imagePreviews.length > 0 && (
|
{imagePreviews.length > 0 && (
|
||||||
<div className="row mt-3">
|
<div className="row mt-3">
|
||||||
{imagePreviews.map((preview, index) => (
|
{imagePreviews.map((preview, index) => (
|
||||||
<div key={index} className="col-6 col-md-4 col-lg-3 mb-3">
|
<div key={index} className="col-6 col-md-4 col-lg-3 mb-3">
|
||||||
<div className="position-relative">
|
<div className="position-relative">
|
||||||
<img
|
<img
|
||||||
src={preview}
|
src={preview}
|
||||||
alt={`Preview ${index + 1}`}
|
alt={`Preview ${index + 1}`}
|
||||||
className="img-fluid rounded"
|
className="img-fluid rounded"
|
||||||
style={{
|
style={{
|
||||||
width: "100%",
|
width: "100%",
|
||||||
height: "150px",
|
height: "150px",
|
||||||
objectFit: "cover",
|
objectFit: "cover",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="btn btn-sm btn-danger position-absolute top-0 end-0 m-1"
|
className="btn btn-sm btn-danger position-absolute top-0 end-0 m-1"
|
||||||
onClick={() => removeImage(index)}
|
onClick={() => removeImage(index)}
|
||||||
>
|
>
|
||||||
<i className="bi bi-x"></i>
|
<i className="bi bi-x"></i>
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Basic Information Card */}
|
||||||
|
<div className="card mb-4">
|
||||||
|
<div className="card-body">
|
||||||
|
<div className="mb-3">
|
||||||
|
<label htmlFor="name" className="form-label">
|
||||||
|
Item Name *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="form-control"
|
||||||
|
id="name"
|
||||||
|
name="name"
|
||||||
|
value={formData.name}
|
||||||
|
onChange={handleChange}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-3">
|
||||||
|
<label htmlFor="description" className="form-label">
|
||||||
|
Description *
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
className="form-control"
|
||||||
|
id="description"
|
||||||
|
name="description"
|
||||||
|
rows={4}
|
||||||
|
value={formData.description}
|
||||||
|
onChange={handleChange}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Location Card */}
|
||||||
|
<div className="card mb-4">
|
||||||
|
<div className="card-body">
|
||||||
|
<div className="row mb-3">
|
||||||
|
<div className="col-md-6">
|
||||||
|
<label htmlFor="address1" className="form-label">
|
||||||
|
Address Line 1 *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="form-control"
|
||||||
|
id="address1"
|
||||||
|
name="address1"
|
||||||
|
value={formData.address1}
|
||||||
|
onChange={handleChange}
|
||||||
|
placeholder="123 Main Street"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="col-md-6">
|
||||||
|
<label htmlFor="address2" className="form-label">
|
||||||
|
Address Line 2
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="form-control"
|
||||||
|
id="address2"
|
||||||
|
name="address2"
|
||||||
|
value={formData.address2}
|
||||||
|
onChange={handleChange}
|
||||||
|
placeholder="Apt, Suite, Unit, etc."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="row mb-3">
|
||||||
|
<div className="col-md-6">
|
||||||
|
<label htmlFor="city" className="form-label">
|
||||||
|
City *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="form-control"
|
||||||
|
id="city"
|
||||||
|
name="city"
|
||||||
|
value={formData.city}
|
||||||
|
onChange={handleChange}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="col-md-3">
|
||||||
|
<label htmlFor="state" className="form-label">
|
||||||
|
State *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="form-control"
|
||||||
|
id="state"
|
||||||
|
name="state"
|
||||||
|
value={formData.state}
|
||||||
|
onChange={handleChange}
|
||||||
|
placeholder="CA"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="col-md-3">
|
||||||
|
<label htmlFor="zipCode" className="form-label">
|
||||||
|
ZIP Code *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="form-control"
|
||||||
|
id="zipCode"
|
||||||
|
name="zipCode"
|
||||||
|
value={formData.zipCode}
|
||||||
|
onChange={handleChange}
|
||||||
|
placeholder="12345"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-3">
|
||||||
|
<label htmlFor="country" className="form-label">
|
||||||
|
Country *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="form-control"
|
||||||
|
id="country"
|
||||||
|
name="country"
|
||||||
|
value={formData.country}
|
||||||
|
onChange={handleChange}
|
||||||
|
placeholder="United States"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Delivery & Availability Card */}
|
||||||
|
<div className="card mb-4">
|
||||||
|
<div className="card-body">
|
||||||
|
<label className="form-label">
|
||||||
|
How will renters receive this item?
|
||||||
|
</label>
|
||||||
|
<div className="form-check">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
className="form-check-input"
|
||||||
|
id="pickUpAvailable"
|
||||||
|
name="pickUpAvailable"
|
||||||
|
checked={formData.pickUpAvailable}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
<label className="form-check-label" htmlFor="pickUpAvailable">
|
||||||
|
Pick-Up
|
||||||
|
<div className="small text-muted">
|
||||||
|
They pick-up the item from your location and they return
|
||||||
|
the item to your location
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className="form-check">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
className="form-check-input"
|
||||||
|
id="localDeliveryAvailable"
|
||||||
|
name="localDeliveryAvailable"
|
||||||
|
checked={formData.localDeliveryAvailable}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
className="form-check-label d-flex align-items-center"
|
||||||
|
htmlFor="localDeliveryAvailable"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
Local Delivery
|
||||||
|
{formData.localDeliveryAvailable && (
|
||||||
|
<span className="ms-2">
|
||||||
|
(Delivery Radius:
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
className="form-control form-control-sm d-inline-block mx-1"
|
||||||
|
id="localDeliveryRadius"
|
||||||
|
name="localDeliveryRadius"
|
||||||
|
value={formData.localDeliveryRadius || ""}
|
||||||
|
onChange={handleChange}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
placeholder="25"
|
||||||
|
min="1"
|
||||||
|
max="100"
|
||||||
|
style={{ width: "60px" }}
|
||||||
|
/>
|
||||||
|
miles)
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<div className="small text-muted">
|
||||||
|
You deliver and then pick-up the item when the rental
|
||||||
|
period ends
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className="form-check">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
className="form-check-input"
|
||||||
|
id="shippingAvailable"
|
||||||
|
name="shippingAvailable"
|
||||||
|
checked={formData.shippingAvailable}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
className="form-check-label"
|
||||||
|
htmlFor="shippingAvailable"
|
||||||
|
>
|
||||||
|
Shipping
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className="form-check">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
className="form-check-input"
|
||||||
|
id="inPlaceUseAvailable"
|
||||||
|
name="inPlaceUseAvailable"
|
||||||
|
checked={formData.inPlaceUseAvailable}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
className="form-check-label"
|
||||||
|
htmlFor="inPlaceUseAvailable"
|
||||||
|
>
|
||||||
|
In-Place Use
|
||||||
|
<div className="small text-muted">
|
||||||
|
They use at your location
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mb-3">
|
|
||||||
<label htmlFor="name" className="form-label">
|
|
||||||
Item Name *
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
className="form-control"
|
|
||||||
id="name"
|
|
||||||
name="name"
|
|
||||||
value={formData.name}
|
|
||||||
onChange={handleChange}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mb-3">
|
|
||||||
<label htmlFor="description" className="form-label">
|
|
||||||
Description *
|
|
||||||
</label>
|
|
||||||
<textarea
|
|
||||||
className="form-control"
|
|
||||||
id="description"
|
|
||||||
name="description"
|
|
||||||
rows={4}
|
|
||||||
value={formData.description}
|
|
||||||
onChange={handleChange}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mb-3">
|
|
||||||
<label className="form-label">Tags</label>
|
|
||||||
<div className="input-group mb-2">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
className="form-control"
|
|
||||||
value={tagInput}
|
|
||||||
onChange={(e) => setTagInput(e.target.value)}
|
|
||||||
onKeyPress={(e) =>
|
|
||||||
e.key === "Enter" && (e.preventDefault(), addTag())
|
|
||||||
}
|
|
||||||
placeholder="Add a tag"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn btn-outline-secondary"
|
|
||||||
onClick={addTag}
|
|
||||||
>
|
|
||||||
Add
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
{formData.tags.map((tag, index) => (
|
|
||||||
<span key={index} className="badge bg-primary me-2 mb-2">
|
|
||||||
{tag}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn-close btn-close-white ms-2"
|
|
||||||
onClick={() => removeTag(tag)}
|
|
||||||
style={{ fontSize: "0.7rem" }}
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h6 className="mb-3">Location *</h6>
|
{/* Pricing Card */}
|
||||||
|
<div className="card mb-4">
|
||||||
<div className="row mb-3">
|
<div className="card-body">
|
||||||
<div className="col-md-6">
|
<div className="mb-3">
|
||||||
<label htmlFor="address1" className="form-label">
|
<div className="row align-items-center">
|
||||||
Address Line 1
|
<div className="col-auto">
|
||||||
</label>
|
<label className="col-form-label">Price per</label>
|
||||||
<input
|
</div>
|
||||||
type="text"
|
<div className="col-auto">
|
||||||
className="form-control"
|
<select
|
||||||
id="address1"
|
className="form-select"
|
||||||
name="address1"
|
value={priceType}
|
||||||
value={formData.address1}
|
onChange={(e) =>
|
||||||
onChange={handleChange}
|
setPriceType(e.target.value as "hour" | "day")
|
||||||
placeholder="123 Main Street"
|
}
|
||||||
required
|
>
|
||||||
/>
|
<option value="hour">Hour</option>
|
||||||
</div>
|
<option value="day">Day</option>
|
||||||
<div className="col-md-6">
|
</select>
|
||||||
<label htmlFor="address2" className="form-label">
|
</div>
|
||||||
Address Line 2
|
<div className="col">
|
||||||
</label>
|
<div className="input-group">
|
||||||
<input
|
<span className="input-group-text">$</span>
|
||||||
type="text"
|
|
||||||
className="form-control"
|
|
||||||
id="address2"
|
|
||||||
name="address2"
|
|
||||||
value={formData.address2}
|
|
||||||
onChange={handleChange}
|
|
||||||
placeholder="Apt, Suite, Unit, etc."
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="row mb-3">
|
|
||||||
<div className="col-md-6">
|
|
||||||
<label htmlFor="city" className="form-label">
|
|
||||||
City
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
className="form-control"
|
|
||||||
id="city"
|
|
||||||
name="city"
|
|
||||||
value={formData.city}
|
|
||||||
onChange={handleChange}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="col-md-3">
|
|
||||||
<label htmlFor="state" className="form-label">
|
|
||||||
State
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
className="form-control"
|
|
||||||
id="state"
|
|
||||||
name="state"
|
|
||||||
value={formData.state}
|
|
||||||
onChange={handleChange}
|
|
||||||
placeholder="CA"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="col-md-3">
|
|
||||||
<label htmlFor="zipCode" className="form-label">
|
|
||||||
ZIP Code
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
className="form-control"
|
|
||||||
id="zipCode"
|
|
||||||
name="zipCode"
|
|
||||||
value={formData.zipCode}
|
|
||||||
onChange={handleChange}
|
|
||||||
placeholder="12345"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mb-3">
|
|
||||||
<label htmlFor="country" className="form-label">
|
|
||||||
Country
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
className="form-control"
|
|
||||||
id="country"
|
|
||||||
name="country"
|
|
||||||
value={formData.country}
|
|
||||||
onChange={handleChange}
|
|
||||||
placeholder="United States"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mb-3">
|
|
||||||
<label className="form-label">Availability Type</label>
|
|
||||||
<div className="form-check">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
className="form-check-input"
|
|
||||||
id="pickUpAvailable"
|
|
||||||
name="pickUpAvailable"
|
|
||||||
checked={formData.pickUpAvailable}
|
|
||||||
onChange={handleChange}
|
|
||||||
/>
|
|
||||||
<label className="form-check-label" htmlFor="pickUpAvailable">
|
|
||||||
Pick-Up
|
|
||||||
<div className="small text-muted">
|
|
||||||
They pick-up the item from your location and they return the
|
|
||||||
item to your location
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div className="form-check">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
className="form-check-input"
|
|
||||||
id="localDeliveryAvailable"
|
|
||||||
name="localDeliveryAvailable"
|
|
||||||
checked={formData.localDeliveryAvailable}
|
|
||||||
onChange={handleChange}
|
|
||||||
/>
|
|
||||||
<label
|
|
||||||
className="form-check-label d-flex align-items-center"
|
|
||||||
htmlFor="localDeliveryAvailable"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
Local Delivery
|
|
||||||
{formData.localDeliveryAvailable && (
|
|
||||||
<span className="ms-2">
|
|
||||||
(Delivery Radius:
|
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
className="form-control form-control-sm d-inline-block mx-1"
|
className="form-control"
|
||||||
id="localDeliveryRadius"
|
id={
|
||||||
name="localDeliveryRadius"
|
priceType === "hour"
|
||||||
value={formData.localDeliveryRadius || ""}
|
? "pricePerHour"
|
||||||
|
: "pricePerDay"
|
||||||
|
}
|
||||||
|
name={
|
||||||
|
priceType === "hour"
|
||||||
|
? "pricePerHour"
|
||||||
|
: "pricePerDay"
|
||||||
|
}
|
||||||
|
value={
|
||||||
|
priceType === "hour"
|
||||||
|
? formData.pricePerHour || ""
|
||||||
|
: formData.pricePerDay || ""
|
||||||
|
}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
onClick={(e) => e.stopPropagation()}
|
step="0.01"
|
||||||
placeholder="25"
|
min="0"
|
||||||
min="1"
|
placeholder="0.00"
|
||||||
max="100"
|
|
||||||
style={{ width: "60px" }}
|
|
||||||
/>
|
/>
|
||||||
miles)
|
</div>
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
<div className="small text-muted">
|
|
||||||
You deliver and then pick-up the item when the rental
|
|
||||||
period ends
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</div>
|
||||||
</div>
|
|
||||||
<div className="form-check">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
className="form-check-input"
|
|
||||||
id="shippingAvailable"
|
|
||||||
name="shippingAvailable"
|
|
||||||
checked={formData.shippingAvailable}
|
|
||||||
onChange={handleChange}
|
|
||||||
/>
|
|
||||||
<label className="form-check-label" htmlFor="shippingAvailable">
|
|
||||||
Shipping
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div className="form-check">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
className="form-check-input"
|
|
||||||
id="inPlaceUseAvailable"
|
|
||||||
name="inPlaceUseAvailable"
|
|
||||||
checked={formData.inPlaceUseAvailable}
|
|
||||||
onChange={handleChange}
|
|
||||||
/>
|
|
||||||
<label
|
|
||||||
className="form-check-label"
|
|
||||||
htmlFor="inPlaceUseAvailable"
|
|
||||||
>
|
|
||||||
In-Place Use
|
|
||||||
<div className="small text-muted">
|
|
||||||
They use at your location
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mb-3">
|
<div className="mb-3">
|
||||||
<div className="row align-items-center">
|
<label htmlFor="minimumRentalDays" className="form-label">
|
||||||
<div className="col-auto">
|
Minimum Rental {priceType === "hour" ? "Hours" : "Days"}
|
||||||
<label className="col-form-label">Price per</label>
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
className="form-control"
|
||||||
|
id="minimumRentalDays"
|
||||||
|
name="minimumRentalDays"
|
||||||
|
value={formData.minimumRentalDays}
|
||||||
|
onChange={handleChange}
|
||||||
|
min="1"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-auto">
|
|
||||||
<select
|
<div className="mb-3">
|
||||||
className="form-select"
|
<label htmlFor="replacementCost" className="form-label">
|
||||||
value={priceType}
|
Replacement Cost *
|
||||||
onChange={(e) =>
|
</label>
|
||||||
setPriceType(e.target.value as "hour" | "day")
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<option value="hour">Hour</option>
|
|
||||||
<option value="day">Day</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div className="col">
|
|
||||||
<div className="input-group">
|
<div className="input-group">
|
||||||
<span className="input-group-text">$</span>
|
<span className="input-group-text">$</span>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
className="form-control"
|
className="form-control"
|
||||||
id={priceType === "hour" ? "pricePerHour" : "pricePerDay"}
|
id="replacementCost"
|
||||||
name={
|
name="replacementCost"
|
||||||
priceType === "hour" ? "pricePerHour" : "pricePerDay"
|
value={formData.replacementCost}
|
||||||
}
|
|
||||||
value={
|
|
||||||
priceType === "hour"
|
|
||||||
? formData.pricePerHour || ""
|
|
||||||
: formData.pricePerDay || ""
|
|
||||||
}
|
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
step="0.01"
|
step="0.01"
|
||||||
min="0"
|
min="0"
|
||||||
placeholder="0.00"
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="form-text">
|
||||||
|
The cost to replace the item if damaged or lost
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mb-3">
|
{/* Availability Schedule Card */}
|
||||||
<label htmlFor="minimumRentalDays" className="form-label">
|
<div className="card mb-4">
|
||||||
Minimum Rental {priceType === "hour" ? "Hours" : "Days"}
|
<div className="card-body">
|
||||||
</label>
|
<p className="text-muted">
|
||||||
<input
|
Select dates when the item is NOT available for rent
|
||||||
type="number"
|
</p>
|
||||||
className="form-control"
|
<AvailabilityCalendar
|
||||||
id="minimumRentalDays"
|
unavailablePeriods={formData.unavailablePeriods || []}
|
||||||
name="minimumRentalDays"
|
onPeriodsChange={(periods) =>
|
||||||
value={formData.minimumRentalDays}
|
setFormData((prev) => ({
|
||||||
onChange={handleChange}
|
...prev,
|
||||||
min="1"
|
unavailablePeriods: periods,
|
||||||
/>
|
}))
|
||||||
</div>
|
}
|
||||||
|
mode="owner"
|
||||||
<div className="mb-4">
|
|
||||||
<h5>Availability Schedule</h5>
|
|
||||||
<p className="text-muted">
|
|
||||||
Select dates when the item is NOT available for rent
|
|
||||||
</p>
|
|
||||||
<AvailabilityCalendar
|
|
||||||
unavailablePeriods={formData.unavailablePeriods || []}
|
|
||||||
onPeriodsChange={(periods) =>
|
|
||||||
setFormData((prev) => ({
|
|
||||||
...prev,
|
|
||||||
unavailablePeriods: periods,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
mode="owner"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mb-3">
|
|
||||||
<label htmlFor="rules" className="form-label">
|
|
||||||
Rental Rules & Guidelines
|
|
||||||
</label>
|
|
||||||
<div className="form-check mb-2">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
className="form-check-input"
|
|
||||||
id="needsTraining"
|
|
||||||
name="needsTraining"
|
|
||||||
checked={formData.needsTraining}
|
|
||||||
onChange={handleChange}
|
|
||||||
/>
|
/>
|
||||||
<label className="form-check-label" htmlFor="needsTraining">
|
</div>
|
||||||
Requires in-person training before rental
|
</div>
|
||||||
|
|
||||||
|
{/* Rules & Guidelines Card */}
|
||||||
|
<div className="card mb-4">
|
||||||
|
<div className="card-body">
|
||||||
|
<div className="form-check mb-3">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
className="form-check-input"
|
||||||
|
id="needsTraining"
|
||||||
|
name="needsTraining"
|
||||||
|
checked={formData.needsTraining}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
<label className="form-check-label" htmlFor="needsTraining">
|
||||||
|
Requires in-person training before rental
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<label htmlFor="rules" className="form-label">
|
||||||
|
Additional Rules or Guidelines
|
||||||
</label>
|
</label>
|
||||||
</div>
|
<textarea
|
||||||
<textarea
|
|
||||||
className="form-control"
|
|
||||||
id="rules"
|
|
||||||
name="rules"
|
|
||||||
rows={3}
|
|
||||||
value={formData.rules || ""}
|
|
||||||
onChange={handleChange}
|
|
||||||
placeholder="Any specific rules or guidelines for renting this item"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mb-3">
|
|
||||||
<label htmlFor="replacementCost" className="form-label">
|
|
||||||
Replacement Cost *
|
|
||||||
</label>
|
|
||||||
<div className="input-group">
|
|
||||||
<span className="input-group-text">$</span>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
className="form-control"
|
className="form-control"
|
||||||
id="replacementCost"
|
id="rules"
|
||||||
name="replacementCost"
|
name="rules"
|
||||||
value={formData.replacementCost}
|
rows={3}
|
||||||
|
value={formData.rules || ""}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
step="0.01"
|
placeholder="Any specific rules or guidelines for renting this item"
|
||||||
min="0"
|
|
||||||
required
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="form-text">
|
|
||||||
The cost to replace the item if damaged or lost
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="d-grid gap-2">
|
<div className="d-grid gap-2">
|
||||||
|
|||||||
@@ -1,15 +1,14 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from "react";
|
||||||
import { useParams, useNavigate } from 'react-router-dom';
|
import { useParams, useNavigate } from "react-router-dom";
|
||||||
import { Item, Rental } from '../types';
|
import { Item, Rental } from "../types";
|
||||||
import { useAuth } from '../contexts/AuthContext';
|
import { useAuth } from "../contexts/AuthContext";
|
||||||
import { itemAPI, rentalAPI } from '../services/api';
|
import { itemAPI, rentalAPI } from "../services/api";
|
||||||
import AvailabilityCalendar from '../components/AvailabilityCalendar';
|
import AvailabilityCalendar from "../components/AvailabilityCalendar";
|
||||||
import AddressAutocomplete from '../components/AddressAutocomplete';
|
import AddressAutocomplete from "../components/AddressAutocomplete";
|
||||||
|
|
||||||
interface ItemFormData {
|
interface ItemFormData {
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
tags: string[];
|
|
||||||
pickUpAvailable: boolean;
|
pickUpAvailable: boolean;
|
||||||
localDeliveryAvailable: boolean;
|
localDeliveryAvailable: boolean;
|
||||||
localDeliveryRadius?: number;
|
localDeliveryRadius?: number;
|
||||||
@@ -41,15 +40,13 @@ const EditItem: React.FC = () => {
|
|||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [success, setSuccess] = useState(false);
|
const [success, setSuccess] = useState(false);
|
||||||
const [tagInput, setTagInput] = useState("");
|
|
||||||
const [imageFiles, setImageFiles] = useState<File[]>([]);
|
const [imageFiles, setImageFiles] = useState<File[]>([]);
|
||||||
const [imagePreviews, setImagePreviews] = useState<string[]>([]);
|
const [imagePreviews, setImagePreviews] = useState<string[]>([]);
|
||||||
const [priceType, setPriceType] = useState<"hour" | "day">("day");
|
const [priceType, setPriceType] = useState<"hour" | "day">("day");
|
||||||
const [acceptedRentals, setAcceptedRentals] = useState<Rental[]>([]);
|
const [acceptedRentals, setAcceptedRentals] = useState<Rental[]>([]);
|
||||||
const [formData, setFormData] = useState<ItemFormData>({
|
const [formData, setFormData] = useState<ItemFormData>({
|
||||||
name: '',
|
name: "",
|
||||||
description: '',
|
description: "",
|
||||||
tags: [],
|
|
||||||
pickUpAvailable: false,
|
pickUpAvailable: false,
|
||||||
localDeliveryAvailable: false,
|
localDeliveryAvailable: false,
|
||||||
shippingAvailable: false,
|
shippingAvailable: false,
|
||||||
@@ -57,8 +54,8 @@ const EditItem: React.FC = () => {
|
|||||||
pricePerHour: undefined,
|
pricePerHour: undefined,
|
||||||
pricePerDay: undefined,
|
pricePerDay: undefined,
|
||||||
replacementCost: 0,
|
replacementCost: 0,
|
||||||
location: '',
|
location: "",
|
||||||
rules: '',
|
rules: "",
|
||||||
minimumRentalDays: 1,
|
minimumRentalDays: 1,
|
||||||
needsTraining: false,
|
needsTraining: false,
|
||||||
availability: true,
|
availability: true,
|
||||||
@@ -74,24 +71,23 @@ const EditItem: React.FC = () => {
|
|||||||
try {
|
try {
|
||||||
const response = await itemAPI.getItem(id!);
|
const response = await itemAPI.getItem(id!);
|
||||||
const item: Item = response.data;
|
const item: Item = response.data;
|
||||||
|
|
||||||
if (item.ownerId !== user?.id) {
|
if (item.ownerId !== user?.id) {
|
||||||
setError('You are not authorized to edit this item');
|
setError("You are not authorized to edit this item");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set the price type based on available pricing
|
// Set the price type based on available pricing
|
||||||
if (item.pricePerHour) {
|
if (item.pricePerHour) {
|
||||||
setPriceType('hour');
|
setPriceType("hour");
|
||||||
} else if (item.pricePerDay) {
|
} else if (item.pricePerDay) {
|
||||||
setPriceType('day');
|
setPriceType("day");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert item data to form data format
|
// Convert item data to form data format
|
||||||
setFormData({
|
setFormData({
|
||||||
name: item.name,
|
name: item.name,
|
||||||
description: item.description,
|
description: item.description,
|
||||||
tags: item.tags || [],
|
|
||||||
pickUpAvailable: item.pickUpAvailable || false,
|
pickUpAvailable: item.pickUpAvailable || false,
|
||||||
localDeliveryAvailable: item.localDeliveryAvailable || false,
|
localDeliveryAvailable: item.localDeliveryAvailable || false,
|
||||||
localDeliveryRadius: item.localDeliveryRadius || 25,
|
localDeliveryRadius: item.localDeliveryRadius || 25,
|
||||||
@@ -103,7 +99,7 @@ const EditItem: React.FC = () => {
|
|||||||
location: item.location,
|
location: item.location,
|
||||||
latitude: item.latitude,
|
latitude: item.latitude,
|
||||||
longitude: item.longitude,
|
longitude: item.longitude,
|
||||||
rules: item.rules || '',
|
rules: item.rules || "",
|
||||||
minimumRentalDays: item.minimumRentalDays,
|
minimumRentalDays: item.minimumRentalDays,
|
||||||
needsTraining: item.needsTraining || false,
|
needsTraining: item.needsTraining || false,
|
||||||
availability: item.availability,
|
availability: item.availability,
|
||||||
@@ -115,7 +111,7 @@ const EditItem: React.FC = () => {
|
|||||||
setImagePreviews(item.images);
|
setImagePreviews(item.images);
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
setError(err.response?.data?.message || 'Failed to fetch item');
|
setError(err.response?.data?.message || "Failed to fetch item");
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -126,13 +122,14 @@ const EditItem: React.FC = () => {
|
|||||||
const response = await rentalAPI.getMyListings();
|
const response = await rentalAPI.getMyListings();
|
||||||
const rentals: Rental[] = response.data;
|
const rentals: Rental[] = response.data;
|
||||||
// Filter for accepted rentals for this specific item
|
// Filter for accepted rentals for this specific item
|
||||||
const itemRentals = rentals.filter(rental =>
|
const itemRentals = rentals.filter(
|
||||||
rental.itemId === id &&
|
(rental) =>
|
||||||
['confirmed', 'active'].includes(rental.status)
|
rental.itemId === id &&
|
||||||
|
["confirmed", "active"].includes(rental.status)
|
||||||
);
|
);
|
||||||
setAcceptedRentals(itemRentals);
|
setAcceptedRentals(itemRentals);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error fetching rentals:', err);
|
console.error("Error fetching rentals:", err);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -175,27 +172,10 @@ const EditItem: React.FC = () => {
|
|||||||
navigate(`/items/${id}`);
|
navigate(`/items/${id}`);
|
||||||
}, 1500);
|
}, 1500);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
setError(err.response?.data?.message || 'Failed to update item');
|
setError(err.response?.data?.message || "Failed to update item");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const addTag = () => {
|
|
||||||
if (tagInput.trim() && !formData.tags.includes(tagInput.trim())) {
|
|
||||||
setFormData((prev) => ({
|
|
||||||
...prev,
|
|
||||||
tags: [...prev.tags, tagInput.trim()],
|
|
||||||
}));
|
|
||||||
setTagInput("");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const removeTag = (tag: string) => {
|
|
||||||
setFormData((prev) => ({
|
|
||||||
...prev,
|
|
||||||
tags: prev.tags.filter((t) => t !== tag),
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleImageChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleImageChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const files = Array.from(e.target.files || []);
|
const files = Array.from(e.target.files || []);
|
||||||
|
|
||||||
@@ -234,7 +214,7 @@ const EditItem: React.FC = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error && error.includes('authorized')) {
|
if (error && error.includes("authorized")) {
|
||||||
return (
|
return (
|
||||||
<div className="container mt-5">
|
<div className="container mt-5">
|
||||||
<div className="alert alert-danger" role="alert">
|
<div className="alert alert-danger" role="alert">
|
||||||
@@ -249,370 +229,397 @@ const EditItem: React.FC = () => {
|
|||||||
<div className="row justify-content-center">
|
<div className="row justify-content-center">
|
||||||
<div className="col-md-8">
|
<div className="col-md-8">
|
||||||
<h1>Edit Listing</h1>
|
<h1>Edit Listing</h1>
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
<div className="alert alert-danger" role="alert">
|
<div className="alert alert-danger" role="alert">
|
||||||
{error}
|
{error}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{success && (
|
{success && (
|
||||||
<div className="alert alert-success" role="alert">
|
<div className="alert alert-success" role="alert">
|
||||||
Item updated successfully! Redirecting...
|
Item updated successfully! Redirecting...
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<form onSubmit={handleSubmit}>
|
|
||||||
<div className="mb-3">
|
|
||||||
<label className="form-label">Images (Max 5)</label>
|
|
||||||
<input
|
|
||||||
type="file"
|
|
||||||
className="form-control"
|
|
||||||
onChange={handleImageChange}
|
|
||||||
accept="image/*"
|
|
||||||
multiple
|
|
||||||
disabled={imagePreviews.length >= 5}
|
|
||||||
/>
|
|
||||||
<div className="form-text">
|
|
||||||
Upload up to 5 images of your item
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{imagePreviews.length > 0 && (
|
<form onSubmit={handleSubmit}>
|
||||||
<div className="row mt-3">
|
{/* Images Card */}
|
||||||
{imagePreviews.map((preview, index) => (
|
<div className="card mb-4">
|
||||||
<div key={index} className="col-6 col-md-4 col-lg-3 mb-3">
|
<div className="card-body">
|
||||||
<div className="position-relative">
|
<div className="mb-3">
|
||||||
<img
|
<label className="form-label">Images (Max 5)</label>
|
||||||
src={preview}
|
<input
|
||||||
alt={`Preview ${index + 1}`}
|
type="file"
|
||||||
className="img-fluid rounded"
|
className="form-control"
|
||||||
style={{
|
onChange={handleImageChange}
|
||||||
width: "100%",
|
accept="image/*"
|
||||||
height: "150px",
|
multiple
|
||||||
objectFit: "cover",
|
disabled={imagePreviews.length >= 5}
|
||||||
}}
|
/>
|
||||||
/>
|
<div className="form-text">
|
||||||
<button
|
Upload up to 5 images of your item
|
||||||
type="button"
|
</div>
|
||||||
className="btn btn-sm btn-danger position-absolute top-0 end-0 m-1"
|
</div>
|
||||||
onClick={() => removeImage(index)}
|
|
||||||
>
|
{imagePreviews.length > 0 && (
|
||||||
<i className="bi bi-x"></i>
|
<div className="row mt-3">
|
||||||
</button>
|
{imagePreviews.map((preview, index) => (
|
||||||
|
<div key={index} className="col-6 col-md-4 col-lg-3 mb-3">
|
||||||
|
<div className="position-relative">
|
||||||
|
<img
|
||||||
|
src={preview}
|
||||||
|
alt={`Preview ${index + 1}`}
|
||||||
|
className="img-fluid rounded"
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
height: "150px",
|
||||||
|
objectFit: "cover",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-sm btn-danger position-absolute top-0 end-0 m-1"
|
||||||
|
onClick={() => removeImage(index)}
|
||||||
|
>
|
||||||
|
<i className="bi bi-x"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Basic Information Card */}
|
||||||
|
<div className="card mb-4">
|
||||||
|
<div className="card-body">
|
||||||
|
<div className="mb-3">
|
||||||
|
<label htmlFor="name" className="form-label">
|
||||||
|
Item Name *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="form-control"
|
||||||
|
id="name"
|
||||||
|
name="name"
|
||||||
|
value={formData.name}
|
||||||
|
onChange={handleChange}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-3">
|
||||||
|
<label htmlFor="description" className="form-label">
|
||||||
|
Description *
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
className="form-control"
|
||||||
|
id="description"
|
||||||
|
name="description"
|
||||||
|
rows={4}
|
||||||
|
value={formData.description}
|
||||||
|
onChange={handleChange}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Location Card */}
|
||||||
|
<div className="card mb-4">
|
||||||
|
<div className="card-body">
|
||||||
|
<div className="mb-3">
|
||||||
|
<label htmlFor="location" className="form-label">
|
||||||
|
Address *
|
||||||
|
</label>
|
||||||
|
<AddressAutocomplete
|
||||||
|
id="location"
|
||||||
|
name="location"
|
||||||
|
value={formData.location}
|
||||||
|
onChange={(value, lat, lon) => {
|
||||||
|
setFormData((prev) => ({
|
||||||
|
...prev,
|
||||||
|
location: value,
|
||||||
|
latitude: lat,
|
||||||
|
longitude: lon,
|
||||||
|
}));
|
||||||
|
}}
|
||||||
|
placeholder="Enter address"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Delivery & Availability Options Card */}
|
||||||
|
<div className="card mb-4">
|
||||||
|
<div className="card-body">
|
||||||
|
<label className="form-label">
|
||||||
|
How will renters receive this item?
|
||||||
|
</label>
|
||||||
|
<div className="form-check">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
className="form-check-input"
|
||||||
|
id="pickUpAvailable"
|
||||||
|
name="pickUpAvailable"
|
||||||
|
checked={formData.pickUpAvailable}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
<label className="form-check-label" htmlFor="pickUpAvailable">
|
||||||
|
Pick-Up
|
||||||
|
<div className="small text-muted">
|
||||||
|
They pick-up the item from your location and they return
|
||||||
|
the item to your location
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className="form-check">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
className="form-check-input"
|
||||||
|
id="localDeliveryAvailable"
|
||||||
|
name="localDeliveryAvailable"
|
||||||
|
checked={formData.localDeliveryAvailable}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
className="form-check-label d-flex align-items-center"
|
||||||
|
htmlFor="localDeliveryAvailable"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
Local Delivery
|
||||||
|
{formData.localDeliveryAvailable && (
|
||||||
|
<span className="ms-2">
|
||||||
|
(Delivery Radius:
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
className="form-control form-control-sm d-inline-block mx-1"
|
||||||
|
id="localDeliveryRadius"
|
||||||
|
name="localDeliveryRadius"
|
||||||
|
value={formData.localDeliveryRadius || ""}
|
||||||
|
onChange={handleChange}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
placeholder="25"
|
||||||
|
min="1"
|
||||||
|
max="100"
|
||||||
|
style={{ width: "60px" }}
|
||||||
|
/>
|
||||||
|
miles)
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<div className="small text-muted">
|
||||||
|
You deliver and then pick-up the item when the rental
|
||||||
|
period ends
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className="form-check">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
className="form-check-input"
|
||||||
|
id="shippingAvailable"
|
||||||
|
name="shippingAvailable"
|
||||||
|
checked={formData.shippingAvailable}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
className="form-check-label"
|
||||||
|
htmlFor="shippingAvailable"
|
||||||
|
>
|
||||||
|
Shipping
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className="form-check">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
className="form-check-input"
|
||||||
|
id="inPlaceUseAvailable"
|
||||||
|
name="inPlaceUseAvailable"
|
||||||
|
checked={formData.inPlaceUseAvailable}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
className="form-check-label"
|
||||||
|
htmlFor="inPlaceUseAvailable"
|
||||||
|
>
|
||||||
|
In-Place Use
|
||||||
|
<div className="small text-muted">
|
||||||
|
They use at your location
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mb-3">
|
|
||||||
<label htmlFor="name" className="form-label">
|
|
||||||
Item Name *
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
className="form-control"
|
|
||||||
id="name"
|
|
||||||
name="name"
|
|
||||||
value={formData.name}
|
|
||||||
onChange={handleChange}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mb-3">
|
|
||||||
<label htmlFor="description" className="form-label">
|
|
||||||
Description *
|
|
||||||
</label>
|
|
||||||
<textarea
|
|
||||||
className="form-control"
|
|
||||||
id="description"
|
|
||||||
name="description"
|
|
||||||
rows={4}
|
|
||||||
value={formData.description}
|
|
||||||
onChange={handleChange}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mb-3">
|
|
||||||
<label className="form-label">Tags</label>
|
|
||||||
<div className="input-group mb-2">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
className="form-control"
|
|
||||||
value={tagInput}
|
|
||||||
onChange={(e) => setTagInput(e.target.value)}
|
|
||||||
onKeyPress={(e) =>
|
|
||||||
e.key === "Enter" && (e.preventDefault(), addTag())
|
|
||||||
}
|
|
||||||
placeholder="Add a tag"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn btn-outline-secondary"
|
|
||||||
onClick={addTag}
|
|
||||||
>
|
|
||||||
Add
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
{formData.tags.map((tag, index) => (
|
|
||||||
<span key={index} className="badge bg-primary me-2 mb-2">
|
|
||||||
{tag}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn-close btn-close-white ms-2"
|
|
||||||
onClick={() => removeTag(tag)}
|
|
||||||
style={{ fontSize: "0.7rem" }}
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mb-3">
|
{/* Pricing Card */}
|
||||||
<label htmlFor="location" className="form-label">
|
<div className="card mb-4">
|
||||||
Location *
|
<div className="card-body">
|
||||||
</label>
|
<div className="mb-3">
|
||||||
<AddressAutocomplete
|
<div className="row align-items-center">
|
||||||
id="location"
|
<div className="col-auto">
|
||||||
name="location"
|
<label className="col-form-label">Price per</label>
|
||||||
value={formData.location}
|
</div>
|
||||||
onChange={(value, lat, lon) => {
|
<div className="col-auto">
|
||||||
setFormData(prev => ({
|
<select
|
||||||
...prev,
|
className="form-select"
|
||||||
location: value,
|
value={priceType}
|
||||||
latitude: lat,
|
onChange={(e) =>
|
||||||
longitude: lon
|
setPriceType(e.target.value as "hour" | "day")
|
||||||
}));
|
}
|
||||||
}}
|
>
|
||||||
placeholder="Address"
|
<option value="hour">Hour</option>
|
||||||
required
|
<option value="day">Day</option>
|
||||||
/>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="col">
|
||||||
<div className="mb-3">
|
<div className="input-group">
|
||||||
<label className="form-label">Availability Type</label>
|
<span className="input-group-text">$</span>
|
||||||
<div className="form-check">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
className="form-check-input"
|
|
||||||
id="pickUpAvailable"
|
|
||||||
name="pickUpAvailable"
|
|
||||||
checked={formData.pickUpAvailable}
|
|
||||||
onChange={handleChange}
|
|
||||||
/>
|
|
||||||
<label className="form-check-label" htmlFor="pickUpAvailable">
|
|
||||||
Pick-Up
|
|
||||||
<div className="small text-muted">They pick-up the item from your location and they return the item to your location</div>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div className="form-check">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
className="form-check-input"
|
|
||||||
id="localDeliveryAvailable"
|
|
||||||
name="localDeliveryAvailable"
|
|
||||||
checked={formData.localDeliveryAvailable}
|
|
||||||
onChange={handleChange}
|
|
||||||
/>
|
|
||||||
<label
|
|
||||||
className="form-check-label d-flex align-items-center"
|
|
||||||
htmlFor="localDeliveryAvailable"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
Local Delivery
|
|
||||||
{formData.localDeliveryAvailable && (
|
|
||||||
<span className="ms-2">
|
|
||||||
(Delivery Radius:
|
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
className="form-control form-control-sm d-inline-block mx-1"
|
className="form-control"
|
||||||
id="localDeliveryRadius"
|
id={
|
||||||
name="localDeliveryRadius"
|
priceType === "hour"
|
||||||
value={formData.localDeliveryRadius || ''}
|
? "pricePerHour"
|
||||||
|
: "pricePerDay"
|
||||||
|
}
|
||||||
|
name={
|
||||||
|
priceType === "hour"
|
||||||
|
? "pricePerHour"
|
||||||
|
: "pricePerDay"
|
||||||
|
}
|
||||||
|
value={
|
||||||
|
priceType === "hour"
|
||||||
|
? formData.pricePerHour || ""
|
||||||
|
: formData.pricePerDay || ""
|
||||||
|
}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
onClick={(e) => e.stopPropagation()}
|
step="0.01"
|
||||||
placeholder="25"
|
min="0"
|
||||||
min="1"
|
placeholder="0.00"
|
||||||
max="100"
|
|
||||||
style={{ width: '60px' }}
|
|
||||||
/>
|
/>
|
||||||
miles)
|
</div>
|
||||||
</span>
|
</div>
|
||||||
)}
|
|
||||||
<div className="small text-muted">You deliver and then pick-up the item when the rental period ends</div>
|
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</div>
|
||||||
</div>
|
|
||||||
<div className="form-check">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
className="form-check-input"
|
|
||||||
id="shippingAvailable"
|
|
||||||
name="shippingAvailable"
|
|
||||||
checked={formData.shippingAvailable}
|
|
||||||
onChange={handleChange}
|
|
||||||
/>
|
|
||||||
<label className="form-check-label" htmlFor="shippingAvailable">
|
|
||||||
Shipping
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div className="form-check">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
className="form-check-input"
|
|
||||||
id="inPlaceUseAvailable"
|
|
||||||
name="inPlaceUseAvailable"
|
|
||||||
checked={formData.inPlaceUseAvailable}
|
|
||||||
onChange={handleChange}
|
|
||||||
/>
|
|
||||||
<label
|
|
||||||
className="form-check-label"
|
|
||||||
htmlFor="inPlaceUseAvailable"
|
|
||||||
>
|
|
||||||
In-Place Use
|
|
||||||
<div className="small text-muted">They use at your location</div>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mb-3">
|
<div className="mb-3">
|
||||||
<div className="row align-items-center">
|
<label htmlFor="minimumRentalDays" className="form-label">
|
||||||
<div className="col-auto">
|
Minimum Rental {priceType === "hour" ? "Hours" : "Days"}
|
||||||
<label className="col-form-label">Price per</label>
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
className="form-control"
|
||||||
|
id="minimumRentalDays"
|
||||||
|
name="minimumRentalDays"
|
||||||
|
value={formData.minimumRentalDays}
|
||||||
|
onChange={handleChange}
|
||||||
|
min="1"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-auto">
|
|
||||||
<select
|
<div className="mb-3">
|
||||||
className="form-select"
|
<label htmlFor="replacementCost" className="form-label">
|
||||||
value={priceType}
|
Replacement Cost *
|
||||||
onChange={(e) => setPriceType(e.target.value as "hour" | "day")}
|
</label>
|
||||||
>
|
|
||||||
<option value="hour">Hour</option>
|
|
||||||
<option value="day">Day</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div className="col">
|
|
||||||
<div className="input-group">
|
<div className="input-group">
|
||||||
<span className="input-group-text">$</span>
|
<span className="input-group-text">$</span>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
className="form-control"
|
className="form-control"
|
||||||
id={priceType === "hour" ? "pricePerHour" : "pricePerDay"}
|
id="replacementCost"
|
||||||
name={priceType === "hour" ? "pricePerHour" : "pricePerDay"}
|
name="replacementCost"
|
||||||
value={priceType === "hour" ? (formData.pricePerHour || "") : (formData.pricePerDay || "")}
|
value={formData.replacementCost}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
step="0.01"
|
step="0.01"
|
||||||
min="0"
|
min="0"
|
||||||
placeholder="0.00"
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="form-text">
|
||||||
|
The cost to replace the item if damaged or lost
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mb-3">
|
{/* Availability Schedule Card */}
|
||||||
<label htmlFor="minimumRentalDays" className="form-label">
|
<div className="card mb-4">
|
||||||
Minimum Rental {priceType === "hour" ? "Hours" : "Days"}
|
<div className="card-body">
|
||||||
</label>
|
<p className="text-muted">
|
||||||
<input
|
Select dates when the item is NOT available for rent. Dates
|
||||||
type="number"
|
with accepted rentals are shown in purple.
|
||||||
className="form-control"
|
</p>
|
||||||
id="minimumRentalDays"
|
<AvailabilityCalendar
|
||||||
name="minimumRentalDays"
|
unavailablePeriods={[
|
||||||
value={formData.minimumRentalDays}
|
...(formData.unavailablePeriods || []),
|
||||||
onChange={handleChange}
|
...acceptedRentals.map((rental) => ({
|
||||||
min="1"
|
id: `rental-${rental.id}`,
|
||||||
/>
|
startDate: new Date(rental.startDate),
|
||||||
</div>
|
endDate: new Date(rental.endDate),
|
||||||
|
isAcceptedRental: true,
|
||||||
<div className="mb-4">
|
})),
|
||||||
<h5>Availability Schedule</h5>
|
]}
|
||||||
<p className="text-muted">Select dates when the item is NOT available for rent. Dates with accepted rentals are shown in purple.</p>
|
onPeriodsChange={(periods) => {
|
||||||
<AvailabilityCalendar
|
// Filter out accepted rental periods when saving
|
||||||
unavailablePeriods={[
|
const userPeriods = periods.filter(
|
||||||
...(formData.unavailablePeriods || []),
|
(p) => !p.isAcceptedRental
|
||||||
...acceptedRentals.map(rental => ({
|
);
|
||||||
id: `rental-${rental.id}`,
|
setFormData((prev) => ({
|
||||||
startDate: new Date(rental.startDate),
|
...prev,
|
||||||
endDate: new Date(rental.endDate),
|
unavailablePeriods: userPeriods,
|
||||||
isAcceptedRental: true
|
}));
|
||||||
}))
|
}}
|
||||||
]}
|
mode="owner"
|
||||||
onPeriodsChange={(periods) => {
|
|
||||||
// Filter out accepted rental periods when saving
|
|
||||||
const userPeriods = periods.filter(p => !p.isAcceptedRental);
|
|
||||||
setFormData(prev => ({ ...prev, unavailablePeriods: userPeriods }));
|
|
||||||
}}
|
|
||||||
mode="owner"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mb-3">
|
|
||||||
<label htmlFor="rules" className="form-label">
|
|
||||||
Rental Rules & Guidelines
|
|
||||||
</label>
|
|
||||||
<div className="form-check mb-2">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
className="form-check-input"
|
|
||||||
id="needsTraining"
|
|
||||||
name="needsTraining"
|
|
||||||
checked={formData.needsTraining}
|
|
||||||
onChange={handleChange}
|
|
||||||
/>
|
/>
|
||||||
<label className="form-check-label" htmlFor="needsTraining">
|
|
||||||
Requires in-person training before rental
|
<div className="mt-3 form-check">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
className="form-check-input"
|
||||||
|
id="availability"
|
||||||
|
name="availability"
|
||||||
|
checked={formData.availability}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
<label className="form-check-label" htmlFor="availability">
|
||||||
|
Available for rent
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Rules & Guidelines Card */}
|
||||||
|
<div className="card mb-4">
|
||||||
|
<div className="card-body">
|
||||||
|
<div className="form-check mb-3">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
className="form-check-input"
|
||||||
|
id="needsTraining"
|
||||||
|
name="needsTraining"
|
||||||
|
checked={formData.needsTraining}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
<label className="form-check-label" htmlFor="needsTraining">
|
||||||
|
Requires in-person training before rental
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<label htmlFor="rules" className="form-label">
|
||||||
|
Additional Rules or Guidelines
|
||||||
</label>
|
</label>
|
||||||
</div>
|
<textarea
|
||||||
<textarea
|
|
||||||
className="form-control"
|
|
||||||
id="rules"
|
|
||||||
name="rules"
|
|
||||||
rows={3}
|
|
||||||
value={formData.rules || ""}
|
|
||||||
onChange={handleChange}
|
|
||||||
placeholder="Any specific rules or guidelines for renting this item"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mb-3">
|
|
||||||
<label htmlFor="replacementCost" className="form-label">
|
|
||||||
Replacement Cost *
|
|
||||||
</label>
|
|
||||||
<div className="input-group">
|
|
||||||
<span className="input-group-text">$</span>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
className="form-control"
|
className="form-control"
|
||||||
id="replacementCost"
|
id="rules"
|
||||||
name="replacementCost"
|
name="rules"
|
||||||
value={formData.replacementCost}
|
rows={3}
|
||||||
|
value={formData.rules || ""}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
step="0.01"
|
placeholder="Any specific rules or guidelines for renting this item"
|
||||||
min="0"
|
|
||||||
required
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="form-text">
|
|
||||||
The cost to replace the item if damaged or lost
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mb-3 form-check">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
className="form-check-input"
|
|
||||||
id="availability"
|
|
||||||
name="availability"
|
|
||||||
checked={formData.availability}
|
|
||||||
onChange={handleChange}
|
|
||||||
/>
|
|
||||||
<label className="form-check-label" htmlFor="availability">
|
|
||||||
Available for rent
|
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="d-grid gap-2">
|
<div className="d-grid gap-2">
|
||||||
@@ -638,4 +645,4 @@ const EditItem: React.FC = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default EditItem;
|
export default EditItem;
|
||||||
|
|||||||
@@ -146,11 +146,6 @@ const ItemDetail: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="mb-3">
|
|
||||||
{item.tags.map((tag, index) => (
|
|
||||||
<span key={index} className="badge bg-secondary me-2">{tag}</span>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<h5>Description</h5>
|
<h5>Description</h5>
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ const ItemList: React.FC = () => {
|
|||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
const [filterTag, setFilterTag] = useState('');
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchItems();
|
fetchItems();
|
||||||
@@ -32,18 +31,14 @@ const ItemList: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get unique tags from all items
|
|
||||||
const allTags = Array.from(new Set(items.flatMap(item => item.tags || [])));
|
|
||||||
|
|
||||||
// Filter items based on search term and selected tag
|
// Filter items based on search term
|
||||||
const filteredItems = items.filter(item => {
|
const filteredItems = items.filter(item => {
|
||||||
const matchesSearch = searchTerm === '' ||
|
const matchesSearch = searchTerm === '' ||
|
||||||
item.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
item.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
item.description.toLowerCase().includes(searchTerm.toLowerCase());
|
item.description.toLowerCase().includes(searchTerm.toLowerCase());
|
||||||
|
|
||||||
const matchesTag = filterTag === '' || (item.tags && item.tags.includes(filterTag));
|
return matchesSearch;
|
||||||
|
|
||||||
return matchesSearch && matchesTag;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
@@ -82,18 +77,6 @@ const ItemList: React.FC = () => {
|
|||||||
onChange={(e) => setSearchTerm(e.target.value)}
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-md-4">
|
|
||||||
<select
|
|
||||||
className="form-select"
|
|
||||||
value={filterTag}
|
|
||||||
onChange={(e) => setFilterTag(e.target.value)}
|
|
||||||
>
|
|
||||||
<option value="">All Categories</option>
|
|
||||||
{allTags.map(tag => (
|
|
||||||
<option key={tag} value={tag}>{tag}</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div className="col-md-2">
|
<div className="col-md-2">
|
||||||
<span className="text-muted">{filteredItems.length} items found</span>
|
<span className="text-muted">{filteredItems.length} items found</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -124,11 +107,6 @@ const ItemList: React.FC = () => {
|
|||||||
</h5>
|
</h5>
|
||||||
<p className="card-text text-truncate text-dark">{item.description}</p>
|
<p className="card-text text-truncate text-dark">{item.description}</p>
|
||||||
|
|
||||||
<div className="mb-2">
|
|
||||||
{item.tags && item.tags.map((tag, index) => (
|
|
||||||
<span key={index} className="badge bg-secondary me-1">{tag}</span>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mb-3">
|
<div className="mb-3">
|
||||||
{item.pricePerDay && (
|
{item.pricePerDay && (
|
||||||
|
|||||||
@@ -34,7 +34,6 @@ export interface Item {
|
|||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
tags: string[];
|
|
||||||
isPortable: boolean;
|
isPortable: boolean;
|
||||||
pickUpAvailable?: boolean;
|
pickUpAvailable?: boolean;
|
||||||
localDeliveryAvailable?: boolean;
|
localDeliveryAvailable?: boolean;
|
||||||
|
|||||||
Reference in New Issue
Block a user