Image is required for creating an item, required fields actually required, Available After and Available Before defaults changed, delete confirmation modal for deleting an item
This commit is contained in:
@@ -95,11 +95,11 @@ const Item = sequelize.define("Item", {
|
|||||||
},
|
},
|
||||||
availableAfter: {
|
availableAfter: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
defaultValue: "09:00",
|
defaultValue: "00:00",
|
||||||
},
|
},
|
||||||
availableBefore: {
|
availableBefore: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
defaultValue: "17:00",
|
defaultValue: "23:00",
|
||||||
},
|
},
|
||||||
specifyTimesPerDay: {
|
specifyTimesPerDay: {
|
||||||
type: DataTypes.BOOLEAN,
|
type: DataTypes.BOOLEAN,
|
||||||
@@ -108,13 +108,13 @@ const Item = sequelize.define("Item", {
|
|||||||
weeklyTimes: {
|
weeklyTimes: {
|
||||||
type: DataTypes.JSONB,
|
type: DataTypes.JSONB,
|
||||||
defaultValue: {
|
defaultValue: {
|
||||||
sunday: { availableAfter: "09:00", availableBefore: "17:00" },
|
sunday: { availableAfter: "00:00", availableBefore: "23:00" },
|
||||||
monday: { availableAfter: "09:00", availableBefore: "17:00" },
|
monday: { availableAfter: "00:00", availableBefore: "23:00" },
|
||||||
tuesday: { availableAfter: "09:00", availableBefore: "17:00" },
|
tuesday: { availableAfter: "00:00", availableBefore: "23:00" },
|
||||||
wednesday: { availableAfter: "09:00", availableBefore: "17:00" },
|
wednesday: { availableAfter: "00:00", availableBefore: "23:00" },
|
||||||
thursday: { availableAfter: "09:00", availableBefore: "17:00" },
|
thursday: { availableAfter: "00:00", availableBefore: "23:00" },
|
||||||
friday: { availableAfter: "09:00", availableBefore: "17:00" },
|
friday: { availableAfter: "00:00", availableBefore: "23:00" },
|
||||||
saturday: { availableAfter: "09:00", availableBefore: "17:00" },
|
saturday: { availableAfter: "00:00", availableBefore: "23:00" },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
ownerId: {
|
ownerId: {
|
||||||
|
|||||||
@@ -89,11 +89,11 @@ const User = sequelize.define(
|
|||||||
},
|
},
|
||||||
defaultAvailableAfter: {
|
defaultAvailableAfter: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
defaultValue: "09:00",
|
defaultValue: "00:00",
|
||||||
},
|
},
|
||||||
defaultAvailableBefore: {
|
defaultAvailableBefore: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
defaultValue: "17:00",
|
defaultValue: "23:00",
|
||||||
},
|
},
|
||||||
defaultSpecifyTimesPerDay: {
|
defaultSpecifyTimesPerDay: {
|
||||||
type: DataTypes.BOOLEAN,
|
type: DataTypes.BOOLEAN,
|
||||||
@@ -102,13 +102,13 @@ const User = sequelize.define(
|
|||||||
defaultWeeklyTimes: {
|
defaultWeeklyTimes: {
|
||||||
type: DataTypes.JSONB,
|
type: DataTypes.JSONB,
|
||||||
defaultValue: {
|
defaultValue: {
|
||||||
sunday: { availableAfter: "09:00", availableBefore: "17:00" },
|
sunday: { availableAfter: "00:00", availableBefore: "23:00" },
|
||||||
monday: { availableAfter: "09:00", availableBefore: "17:00" },
|
monday: { availableAfter: "00:00", availableBefore: "23:00" },
|
||||||
tuesday: { availableAfter: "09:00", availableBefore: "17:00" },
|
tuesday: { availableAfter: "00:00", availableBefore: "23:00" },
|
||||||
wednesday: { availableAfter: "09:00", availableBefore: "17:00" },
|
wednesday: { availableAfter: "00:00", availableBefore: "23:00" },
|
||||||
thursday: { availableAfter: "09:00", availableBefore: "17:00" },
|
thursday: { availableAfter: "00:00", availableBefore: "23:00" },
|
||||||
friday: { availableAfter: "09:00", availableBefore: "17:00" },
|
friday: { availableAfter: "00:00", availableBefore: "23:00" },
|
||||||
saturday: { availableAfter: "09:00", availableBefore: "17:00" },
|
saturday: { availableAfter: "00:00", availableBefore: "23:00" },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
stripeConnectedAccountId: {
|
stripeConnectedAccountId: {
|
||||||
|
|||||||
@@ -328,12 +328,37 @@ router.post("/", authenticateToken, requireVerifiedEmail, async (req, res, next)
|
|||||||
// Extract only allowed fields (prevents mass assignment)
|
// Extract only allowed fields (prevents mass assignment)
|
||||||
const allowedData = extractAllowedFields(req.body);
|
const allowedData = extractAllowedFields(req.body);
|
||||||
|
|
||||||
// Validate imageFilenames if provided
|
// Validate imageFilenames - at least one image is required
|
||||||
if (allowedData.imageFilenames) {
|
|
||||||
const imageFilenames = Array.isArray(allowedData.imageFilenames)
|
const imageFilenames = Array.isArray(allowedData.imageFilenames)
|
||||||
? allowedData.imageFilenames
|
? allowedData.imageFilenames
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
|
if (imageFilenames.length === 0) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: "At least one image is required to create a listing"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate required fields
|
||||||
|
if (!allowedData.name || !allowedData.name.trim()) {
|
||||||
|
return res.status(400).json({ error: "Item name is required" });
|
||||||
|
}
|
||||||
|
if (!allowedData.address1 || !allowedData.address1.trim()) {
|
||||||
|
return res.status(400).json({ error: "Address is required" });
|
||||||
|
}
|
||||||
|
if (!allowedData.city || !allowedData.city.trim()) {
|
||||||
|
return res.status(400).json({ error: "City is required" });
|
||||||
|
}
|
||||||
|
if (!allowedData.state || !allowedData.state.trim()) {
|
||||||
|
return res.status(400).json({ error: "State is required" });
|
||||||
|
}
|
||||||
|
if (!allowedData.zipCode || !allowedData.zipCode.trim()) {
|
||||||
|
return res.status(400).json({ error: "ZIP code is required" });
|
||||||
|
}
|
||||||
|
if (!allowedData.replacementCost || Number(allowedData.replacementCost) <= 0) {
|
||||||
|
return res.status(400).json({ error: "Replacement cost is required" });
|
||||||
|
}
|
||||||
|
|
||||||
const keyValidation = validateS3Keys(imageFilenames, 'items', { maxKeys: IMAGE_LIMITS.items });
|
const keyValidation = validateS3Keys(imageFilenames, 'items', { maxKeys: IMAGE_LIMITS.items });
|
||||||
if (!keyValidation.valid) {
|
if (!keyValidation.valid) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
@@ -342,7 +367,6 @@ router.post("/", authenticateToken, requireVerifiedEmail, async (req, res, next)
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
allowedData.imageFilenames = imageFilenames;
|
allowedData.imageFilenames = imageFilenames;
|
||||||
}
|
|
||||||
|
|
||||||
const item = await Item.create({
|
const item = await Item.create({
|
||||||
...allowedData,
|
...allowedData,
|
||||||
@@ -428,6 +452,13 @@ router.put("/:id", authenticateToken, async (req, res, next) => {
|
|||||||
? allowedData.imageFilenames
|
? allowedData.imageFilenames
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
|
// Require at least one image
|
||||||
|
if (imageFilenames.length === 0) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: "At least one image is required for a listing"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const keyValidation = validateS3Keys(imageFilenames, 'items', { maxKeys: IMAGE_LIMITS.items });
|
const keyValidation = validateS3Keys(imageFilenames, 'items', { maxKeys: IMAGE_LIMITS.items });
|
||||||
if (!keyValidation.valid) {
|
if (!keyValidation.valid) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
@@ -438,6 +469,26 @@ router.put("/:id", authenticateToken, async (req, res, next) => {
|
|||||||
allowedData.imageFilenames = imageFilenames;
|
allowedData.imageFilenames = imageFilenames;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate required fields if they are being updated
|
||||||
|
if (allowedData.name !== undefined && (!allowedData.name || !allowedData.name.trim())) {
|
||||||
|
return res.status(400).json({ error: "Item name is required" });
|
||||||
|
}
|
||||||
|
if (allowedData.address1 !== undefined && (!allowedData.address1 || !allowedData.address1.trim())) {
|
||||||
|
return res.status(400).json({ error: "Address is required" });
|
||||||
|
}
|
||||||
|
if (allowedData.city !== undefined && (!allowedData.city || !allowedData.city.trim())) {
|
||||||
|
return res.status(400).json({ error: "City is required" });
|
||||||
|
}
|
||||||
|
if (allowedData.state !== undefined && (!allowedData.state || !allowedData.state.trim())) {
|
||||||
|
return res.status(400).json({ error: "State is required" });
|
||||||
|
}
|
||||||
|
if (allowedData.zipCode !== undefined && (!allowedData.zipCode || !allowedData.zipCode.trim())) {
|
||||||
|
return res.status(400).json({ error: "ZIP code is required" });
|
||||||
|
}
|
||||||
|
if (allowedData.replacementCost !== undefined && (!allowedData.replacementCost || Number(allowedData.replacementCost) <= 0)) {
|
||||||
|
return res.status(400).json({ error: "Replacement cost is required" });
|
||||||
|
}
|
||||||
|
|
||||||
await item.update(allowedData);
|
await item.update(allowedData);
|
||||||
|
|
||||||
const updatedItem = await Item.findByPk(item.id, {
|
const updatedItem = await Item.findByPk(item.id, {
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ const AvailabilitySettings: React.FC<AvailabilitySettingsProps> = ({
|
|||||||
<div className="row mb-3">
|
<div className="row mb-3">
|
||||||
<div className="col-md-6">
|
<div className="col-md-6">
|
||||||
<label htmlFor="generalAvailableAfter" className="form-label">
|
<label htmlFor="generalAvailableAfter" className="form-label">
|
||||||
Available After *
|
Available After
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
className="form-select"
|
className="form-select"
|
||||||
@@ -69,7 +69,6 @@ const AvailabilitySettings: React.FC<AvailabilitySettingsProps> = ({
|
|||||||
value={data.generalAvailableAfter}
|
value={data.generalAvailableAfter}
|
||||||
onChange={handleGeneralChange}
|
onChange={handleGeneralChange}
|
||||||
disabled={data.specifyTimesPerDay}
|
disabled={data.specifyTimesPerDay}
|
||||||
required
|
|
||||||
>
|
>
|
||||||
{generateTimeOptions().map((option) => (
|
{generateTimeOptions().map((option) => (
|
||||||
<option key={option.value} value={option.value}>
|
<option key={option.value} value={option.value}>
|
||||||
@@ -80,7 +79,7 @@ const AvailabilitySettings: React.FC<AvailabilitySettingsProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
<div className="col-md-6">
|
<div className="col-md-6">
|
||||||
<label htmlFor="generalAvailableBefore" className="form-label">
|
<label htmlFor="generalAvailableBefore" className="form-label">
|
||||||
Available Before *
|
Available Before
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
className="form-select"
|
className="form-select"
|
||||||
@@ -89,7 +88,6 @@ const AvailabilitySettings: React.FC<AvailabilitySettingsProps> = ({
|
|||||||
value={data.generalAvailableBefore}
|
value={data.generalAvailableBefore}
|
||||||
onChange={handleGeneralChange}
|
onChange={handleGeneralChange}
|
||||||
disabled={data.specifyTimesPerDay}
|
disabled={data.specifyTimesPerDay}
|
||||||
required
|
|
||||||
>
|
>
|
||||||
{generateTimeOptions().map((option) => (
|
{generateTimeOptions().map((option) => (
|
||||||
<option key={option.value} value={option.value}>
|
<option key={option.value} value={option.value}>
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ const FeedbackButton: React.FC = () => {
|
|||||||
top: 50%;
|
top: 50%;
|
||||||
transform: translateY(-50%);
|
transform: translateY(-50%);
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
background-color: #0d6efd;
|
background-color: #14B8A6;
|
||||||
color: white;
|
color: white;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 8px 0 0 8px;
|
border-radius: 8px 0 0 8px;
|
||||||
@@ -40,13 +40,13 @@ const FeedbackButton: React.FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.feedback-tab:hover {
|
.feedback-tab:hover {
|
||||||
background-color: #0b5ed7;
|
background-color: #0d9488;
|
||||||
padding-right: 14px;
|
padding-right: 14px;
|
||||||
box-shadow: -4px 0 12px rgba(0, 0, 0, 0.2);
|
box-shadow: -4px 0 12px rgba(0, 0, 0, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.feedback-tab:active {
|
.feedback-tab:active {
|
||||||
background-color: #0a58ca;
|
background-color: #0f766e;
|
||||||
}
|
}
|
||||||
|
|
||||||
.feedback-tab-text {
|
.feedback-tab-text {
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ const ImageUpload: React.FC<ImageUploadProps> = ({
|
|||||||
<div className="card-body">
|
<div className="card-body">
|
||||||
<div className="mb-3">
|
<div className="mb-3">
|
||||||
<label className="form-label mb-0">
|
<label className="form-label mb-0">
|
||||||
Upload Images (Max {maxImages})
|
Upload Images (Max {maxImages}) *
|
||||||
</label>
|
</label>
|
||||||
<div className="form-text mb-2">
|
<div className="form-text mb-2">
|
||||||
Have pictures of everything that's included
|
Have pictures of everything that's included
|
||||||
@@ -50,7 +50,7 @@ const ImageUpload: React.FC<ImageUploadProps> = ({
|
|||||||
style={{
|
style={{
|
||||||
width: "100%",
|
width: "100%",
|
||||||
height: "150px",
|
height: "150px",
|
||||||
objectFit: "cover",
|
objectFit: "contain",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -67,13 +67,11 @@ const PricingForm: React.FC<PricingFormProps> = ({
|
|||||||
"Set multiple pricing tiers for flexible rental rates."
|
"Set multiple pricing tiers for flexible rental rates."
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
Set your pricing rate. You can use Advanced Pricing for multiple
|
Community Rentals charges a 10% Community Upkeep Fee to help keep
|
||||||
pricing tiers. Community Rentals charges a 10% Community Upkeep
|
us running.{" "}
|
||||||
Fee to help keep us running.{" "}
|
|
||||||
<Link to="/faq" target="_blank">
|
<Link to="/faq" target="_blank">
|
||||||
Calculate what you can earn here.
|
Calculate what you can earn here
|
||||||
</Link>
|
</Link>
|
||||||
.
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -67,17 +67,17 @@ const CreateItem: React.FC = () => {
|
|||||||
state: "",
|
state: "",
|
||||||
zipCode: "",
|
zipCode: "",
|
||||||
country: "US",
|
country: "US",
|
||||||
generalAvailableAfter: "09:00",
|
generalAvailableAfter: "00:00",
|
||||||
generalAvailableBefore: "17:00",
|
generalAvailableBefore: "23:00",
|
||||||
specifyTimesPerDay: false,
|
specifyTimesPerDay: false,
|
||||||
weeklyTimes: {
|
weeklyTimes: {
|
||||||
sunday: { availableAfter: "09:00", availableBefore: "17:00" },
|
sunday: { availableAfter: "00:00", availableBefore: "23:00" },
|
||||||
monday: { availableAfter: "09:00", availableBefore: "17:00" },
|
monday: { availableAfter: "00:00", availableBefore: "23:00" },
|
||||||
tuesday: { availableAfter: "09:00", availableBefore: "17:00" },
|
tuesday: { availableAfter: "00:00", availableBefore: "23:00" },
|
||||||
wednesday: { availableAfter: "09:00", availableBefore: "17:00" },
|
wednesday: { availableAfter: "00:00", availableBefore: "23:00" },
|
||||||
thursday: { availableAfter: "09:00", availableBefore: "17:00" },
|
thursday: { availableAfter: "00:00", availableBefore: "23:00" },
|
||||||
friday: { availableAfter: "09:00", availableBefore: "17:00" },
|
friday: { availableAfter: "00:00", availableBefore: "23:00" },
|
||||||
saturday: { availableAfter: "09:00", availableBefore: "17:00" },
|
saturday: { availableAfter: "00:00", availableBefore: "23:00" },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const [imageFiles, setImageFiles] = useState<File[]>([]);
|
const [imageFiles, setImageFiles] = useState<File[]>([]);
|
||||||
@@ -163,6 +163,48 @@ const CreateItem: React.FC = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (imageFiles.length === 0) {
|
||||||
|
setError("At least one image is required to create a listing");
|
||||||
|
document.getElementById("image-upload-section")?.scrollIntoView({ behavior: "smooth", block: "center" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!formData.name.trim()) {
|
||||||
|
setError("Item name is required");
|
||||||
|
document.getElementById("name")?.focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!formData.address1.trim()) {
|
||||||
|
setError("Address is required");
|
||||||
|
document.getElementById("address1")?.focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!formData.city.trim()) {
|
||||||
|
setError("City is required");
|
||||||
|
document.getElementById("city")?.focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!formData.state.trim()) {
|
||||||
|
setError("State is required");
|
||||||
|
document.getElementById("state")?.focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!formData.zipCode.trim()) {
|
||||||
|
setError("ZIP code is required");
|
||||||
|
document.getElementById("zipCode")?.focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!formData.replacementCost || Number(formData.replacementCost) <= 0) {
|
||||||
|
setError("Replacement cost is required");
|
||||||
|
document.getElementById("replacementCost")?.focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError("");
|
setError("");
|
||||||
|
|
||||||
@@ -393,6 +435,7 @@ const CreateItem: React.FC = () => {
|
|||||||
|
|
||||||
const newImageFiles = [...imageFiles, ...files];
|
const newImageFiles = [...imageFiles, ...files];
|
||||||
setImageFiles(newImageFiles);
|
setImageFiles(newImageFiles);
|
||||||
|
setError(""); // Clear any previous error
|
||||||
|
|
||||||
// Create previews
|
// Create previews
|
||||||
files.forEach((file) => {
|
files.forEach((file) => {
|
||||||
@@ -456,7 +499,8 @@ const CreateItem: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit} noValidate>
|
||||||
|
<div id="image-upload-section">
|
||||||
<ImageUpload
|
<ImageUpload
|
||||||
imageFiles={imageFiles}
|
imageFiles={imageFiles}
|
||||||
imagePreviews={imagePreviews}
|
imagePreviews={imagePreviews}
|
||||||
@@ -464,6 +508,7 @@ const CreateItem: React.FC = () => {
|
|||||||
onRemoveImage={removeImage}
|
onRemoveImage={removeImage}
|
||||||
error={error}
|
error={error}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<ItemInformation
|
<ItemInformation
|
||||||
name={formData.name}
|
name={formData.name}
|
||||||
|
|||||||
@@ -90,17 +90,17 @@ const EditItem: React.FC = () => {
|
|||||||
zipCode: "",
|
zipCode: "",
|
||||||
country: "US",
|
country: "US",
|
||||||
rules: "",
|
rules: "",
|
||||||
generalAvailableAfter: "09:00",
|
generalAvailableAfter: "00:00",
|
||||||
generalAvailableBefore: "17:00",
|
generalAvailableBefore: "23:00",
|
||||||
specifyTimesPerDay: false,
|
specifyTimesPerDay: false,
|
||||||
weeklyTimes: {
|
weeklyTimes: {
|
||||||
sunday: { availableAfter: "09:00", availableBefore: "17:00" },
|
sunday: { availableAfter: "00:00", availableBefore: "23:00" },
|
||||||
monday: { availableAfter: "09:00", availableBefore: "17:00" },
|
monday: { availableAfter: "00:00", availableBefore: "23:00" },
|
||||||
tuesday: { availableAfter: "09:00", availableBefore: "17:00" },
|
tuesday: { availableAfter: "00:00", availableBefore: "23:00" },
|
||||||
wednesday: { availableAfter: "09:00", availableBefore: "17:00" },
|
wednesday: { availableAfter: "00:00", availableBefore: "23:00" },
|
||||||
thursday: { availableAfter: "09:00", availableBefore: "17:00" },
|
thursday: { availableAfter: "00:00", availableBefore: "23:00" },
|
||||||
friday: { availableAfter: "09:00", availableBefore: "17:00" },
|
friday: { availableAfter: "00:00", availableBefore: "23:00" },
|
||||||
saturday: { availableAfter: "09:00", availableBefore: "17:00" },
|
saturday: { availableAfter: "00:00", availableBefore: "23:00" },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -151,17 +151,17 @@ const EditItem: React.FC = () => {
|
|||||||
latitude: item.latitude,
|
latitude: item.latitude,
|
||||||
longitude: item.longitude,
|
longitude: item.longitude,
|
||||||
rules: item.rules || "",
|
rules: item.rules || "",
|
||||||
generalAvailableAfter: item.availableAfter || "09:00",
|
generalAvailableAfter: item.availableAfter || "00:00",
|
||||||
generalAvailableBefore: item.availableBefore || "17:00",
|
generalAvailableBefore: item.availableBefore || "23:00",
|
||||||
specifyTimesPerDay: item.specifyTimesPerDay || false,
|
specifyTimesPerDay: item.specifyTimesPerDay || false,
|
||||||
weeklyTimes: item.weeklyTimes || {
|
weeklyTimes: item.weeklyTimes || {
|
||||||
sunday: { availableAfter: "09:00", availableBefore: "17:00" },
|
sunday: { availableAfter: "00:00", availableBefore: "23:00" },
|
||||||
monday: { availableAfter: "09:00", availableBefore: "17:00" },
|
monday: { availableAfter: "00:00", availableBefore: "23:00" },
|
||||||
tuesday: { availableAfter: "09:00", availableBefore: "17:00" },
|
tuesday: { availableAfter: "00:00", availableBefore: "23:00" },
|
||||||
wednesday: { availableAfter: "09:00", availableBefore: "17:00" },
|
wednesday: { availableAfter: "00:00", availableBefore: "23:00" },
|
||||||
thursday: { availableAfter: "09:00", availableBefore: "17:00" },
|
thursday: { availableAfter: "00:00", availableBefore: "23:00" },
|
||||||
friday: { availableAfter: "09:00", availableBefore: "17:00" },
|
friday: { availableAfter: "00:00", availableBefore: "23:00" },
|
||||||
saturday: { availableAfter: "09:00", availableBefore: "17:00" },
|
saturday: { availableAfter: "00:00", availableBefore: "23:00" },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -259,6 +259,51 @@ const EditItem: React.FC = () => {
|
|||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
|
// Check total images (existing + new)
|
||||||
|
const totalImages = existingImageKeys.length + imageFiles.length;
|
||||||
|
if (totalImages === 0) {
|
||||||
|
setError("At least one image is required for a listing");
|
||||||
|
document.getElementById("image-upload-section")?.scrollIntoView({ behavior: "smooth", block: "center" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!formData.name.trim()) {
|
||||||
|
setError("Item name is required");
|
||||||
|
document.getElementById("name")?.focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!formData.address1.trim()) {
|
||||||
|
setError("Address is required");
|
||||||
|
document.getElementById("address1")?.focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!formData.city.trim()) {
|
||||||
|
setError("City is required");
|
||||||
|
document.getElementById("city")?.focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!formData.state.trim()) {
|
||||||
|
setError("State is required");
|
||||||
|
document.getElementById("state")?.focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!formData.zipCode.trim()) {
|
||||||
|
setError("ZIP code is required");
|
||||||
|
document.getElementById("zipCode")?.focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!formData.replacementCost || Number(formData.replacementCost) <= 0) {
|
||||||
|
setError("Replacement cost is required");
|
||||||
|
document.getElementById("replacementCost")?.focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setSubmitting(true);
|
setSubmitting(true);
|
||||||
|
|
||||||
// Try to geocode the address before submitting
|
// Try to geocode the address before submitting
|
||||||
@@ -358,6 +403,7 @@ const EditItem: React.FC = () => {
|
|||||||
|
|
||||||
const newImageFiles = [...imageFiles, ...files];
|
const newImageFiles = [...imageFiles, ...files];
|
||||||
setImageFiles(newImageFiles);
|
setImageFiles(newImageFiles);
|
||||||
|
setError(null); // Clear any previous error
|
||||||
|
|
||||||
// Create previews
|
// Create previews
|
||||||
files.forEach((file) => {
|
files.forEach((file) => {
|
||||||
@@ -492,7 +538,8 @@ const EditItem: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit} noValidate>
|
||||||
|
<div id="image-upload-section">
|
||||||
<ImageUpload
|
<ImageUpload
|
||||||
imageFiles={imageFiles}
|
imageFiles={imageFiles}
|
||||||
imagePreviews={imagePreviews}
|
imagePreviews={imagePreviews}
|
||||||
@@ -500,6 +547,7 @@ const EditItem: React.FC = () => {
|
|||||||
onRemoveImage={removeImage}
|
onRemoveImage={removeImage}
|
||||||
error={error || ""}
|
error={error || ""}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<ItemInformation
|
<ItemInformation
|
||||||
name={formData.name}
|
name={formData.name}
|
||||||
|
|||||||
@@ -71,6 +71,8 @@ const Owning: React.FC = () => {
|
|||||||
useState<ConditionCheck | null>(null);
|
useState<ConditionCheck | null>(null);
|
||||||
const [showReturnStatusModal, setShowReturnStatusModal] = useState(false);
|
const [showReturnStatusModal, setShowReturnStatusModal] = useState(false);
|
||||||
const [rentalForReturn, setRentalForReturn] = useState<Rental | null>(null);
|
const [rentalForReturn, setRentalForReturn] = useState<Rental | null>(null);
|
||||||
|
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
||||||
|
const [itemToDelete, setItemToDelete] = useState<Item | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchListings();
|
fetchListings();
|
||||||
@@ -107,15 +109,23 @@ const Owning: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = async (itemId: string) => {
|
const handleDeleteClick = (item: Item) => {
|
||||||
if (!window.confirm("Are you sure you want to delete this listing?"))
|
setItemToDelete(item);
|
||||||
return;
|
setShowDeleteModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteConfirm = async () => {
|
||||||
|
if (!itemToDelete) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await api.delete(`/items/${itemId}`);
|
await api.delete(`/items/${itemToDelete.id}`);
|
||||||
setListings(listings.filter((item) => item.id !== itemId));
|
setListings(listings.filter((item) => item.id !== itemToDelete.id));
|
||||||
|
setShowDeleteModal(false);
|
||||||
|
setItemToDelete(null);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
alert("Failed to delete listing");
|
setError("Failed to delete listing");
|
||||||
|
setShowDeleteModal(false);
|
||||||
|
setItemToDelete(null);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -713,7 +723,7 @@ const Owning: React.FC = () => {
|
|||||||
{item.isAvailable ? "Mark Unavailable" : "Mark Available"}
|
{item.isAvailable ? "Mark Unavailable" : "Mark Available"}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => handleDelete(item.id)}
|
onClick={() => handleDeleteClick(item)}
|
||||||
className="btn btn-sm btn-outline-danger"
|
className="btn btn-sm btn-outline-danger"
|
||||||
>
|
>
|
||||||
Delete
|
Delete
|
||||||
@@ -803,6 +813,54 @@ const Owning: React.FC = () => {
|
|||||||
}}
|
}}
|
||||||
conditionCheck={selectedConditionCheck}
|
conditionCheck={selectedConditionCheck}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Delete Confirmation Modal */}
|
||||||
|
{showDeleteModal && (
|
||||||
|
<div
|
||||||
|
className="modal fade show d-block"
|
||||||
|
tabIndex={-1}
|
||||||
|
style={{ backgroundColor: "rgba(0,0,0,0.5)" }}
|
||||||
|
>
|
||||||
|
<div className="modal-dialog modal-dialog-centered">
|
||||||
|
<div className="modal-content">
|
||||||
|
<div className="modal-header">
|
||||||
|
<h5 className="modal-title">Delete Listing</h5>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn-close"
|
||||||
|
onClick={() => {
|
||||||
|
setShowDeleteModal(false);
|
||||||
|
setItemToDelete(null);
|
||||||
|
}}
|
||||||
|
></button>
|
||||||
|
</div>
|
||||||
|
<div className="modal-body">
|
||||||
|
<p>Are you sure you want to delete {itemToDelete?.name}?</p>
|
||||||
|
<p>This action cannot be undone.</p>
|
||||||
|
</div>
|
||||||
|
<div className="modal-footer">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-secondary"
|
||||||
|
onClick={() => {
|
||||||
|
setShowDeleteModal(false);
|
||||||
|
setItemToDelete(null);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-danger"
|
||||||
|
onClick={handleDeleteConfirm}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user