rental price calculation bug, sticky pricing cards on mobile, bigger font app wide, removed delivery options from frontened, searching by location with zipcode works when there's multiple zipcodes in the area,
This commit is contained in:
@@ -7,7 +7,6 @@ import AvailabilitySettings from "../components/AvailabilitySettings";
|
||||
import ImageUpload from "../components/ImageUpload";
|
||||
import ItemInformation from "../components/ItemInformation";
|
||||
import LocationForm from "../components/LocationForm";
|
||||
import DeliveryOptions from "../components/DeliveryOptions";
|
||||
import PricingForm from "../components/PricingForm";
|
||||
import RulesForm from "../components/RulesForm";
|
||||
import VerificationCodeModal from "../components/VerificationCodeModal";
|
||||
@@ -17,8 +16,6 @@ import { IMAGE_LIMITS } from "../config/imageLimits";
|
||||
interface ItemFormData {
|
||||
name: string;
|
||||
description: string;
|
||||
pickUpAvailable: boolean;
|
||||
inPlaceUseAvailable: boolean;
|
||||
pricePerHour?: number | string;
|
||||
pricePerDay?: number | string;
|
||||
pricePerWeek?: number | string;
|
||||
@@ -57,8 +54,6 @@ const CreateItem: React.FC = () => {
|
||||
const [formData, setFormData] = useState<ItemFormData>({
|
||||
name: "",
|
||||
description: "",
|
||||
pickUpAvailable: false,
|
||||
inPlaceUseAvailable: false,
|
||||
pricePerDay: "",
|
||||
replacementCost: "",
|
||||
address1: "",
|
||||
@@ -539,12 +534,6 @@ const CreateItem: React.FC = () => {
|
||||
}}
|
||||
/>
|
||||
|
||||
<DeliveryOptions
|
||||
pickUpAvailable={formData.pickUpAvailable}
|
||||
inPlaceUseAvailable={formData.inPlaceUseAvailable}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
|
||||
{/* Availability Card */}
|
||||
<div className="card mb-4">
|
||||
<div className="card-body">
|
||||
|
||||
@@ -8,7 +8,6 @@ import AvailabilitySettings from "../components/AvailabilitySettings";
|
||||
import ImageUpload from "../components/ImageUpload";
|
||||
import ItemInformation from "../components/ItemInformation";
|
||||
import LocationForm from "../components/LocationForm";
|
||||
import DeliveryOptions from "../components/DeliveryOptions";
|
||||
import PricingForm from "../components/PricingForm";
|
||||
import RulesForm from "../components/RulesForm";
|
||||
import { IMAGE_LIMITS } from "../config/imageLimits";
|
||||
@@ -16,8 +15,6 @@ import { IMAGE_LIMITS } from "../config/imageLimits";
|
||||
interface ItemFormData {
|
||||
name: string;
|
||||
description: string;
|
||||
pickUpAvailable: boolean;
|
||||
inPlaceUseAvailable: boolean;
|
||||
pricePerHour?: number | string;
|
||||
pricePerDay?: number | string;
|
||||
pricePerWeek?: number | string;
|
||||
@@ -78,8 +75,6 @@ const EditItem: React.FC = () => {
|
||||
const [formData, setFormData] = useState<ItemFormData>({
|
||||
name: "",
|
||||
description: "",
|
||||
pickUpAvailable: false,
|
||||
inPlaceUseAvailable: false,
|
||||
pricePerHour: "",
|
||||
pricePerDay: "",
|
||||
replacementCost: "",
|
||||
@@ -135,8 +130,6 @@ const EditItem: React.FC = () => {
|
||||
setFormData({
|
||||
name: item.name,
|
||||
description: item.description,
|
||||
pickUpAvailable: item.pickUpAvailable || false,
|
||||
inPlaceUseAvailable: item.inPlaceUseAvailable || false,
|
||||
pricePerHour: item.pricePerHour || "",
|
||||
pricePerDay: item.pricePerDay || "",
|
||||
pricePerWeek: item.pricePerWeek || "",
|
||||
@@ -578,12 +571,6 @@ const EditItem: React.FC = () => {
|
||||
}}
|
||||
/>
|
||||
|
||||
<DeliveryOptions
|
||||
pickUpAvailable={formData.pickUpAvailable}
|
||||
inPlaceUseAvailable={formData.inPlaceUseAvailable}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
|
||||
<PricingForm
|
||||
pricePerHour={formData.pricePerHour || ""}
|
||||
pricePerDay={formData.pricePerDay || ""}
|
||||
|
||||
@@ -531,6 +531,7 @@ const ItemDetail: React.FC = () => {
|
||||
<div className="col-md-4">
|
||||
<div
|
||||
className="card sticky-pricing-card"
|
||||
id="pricing-card"
|
||||
style={{
|
||||
position: "sticky",
|
||||
top: "20px",
|
||||
@@ -610,9 +611,9 @@ const ItemDetail: React.FC = () => {
|
||||
<>
|
||||
<hr />
|
||||
<div className="text-start">
|
||||
<div className="mb-2">
|
||||
<label className="form-label small mb-1">Start</label>
|
||||
<div className="input-group input-group-sm">
|
||||
<div className="mb-3">
|
||||
<label className="form-label fw-medium mb-2">Start</label>
|
||||
<div className="input-group input-group-lg">
|
||||
<input
|
||||
type="date"
|
||||
className="form-control"
|
||||
@@ -627,7 +628,7 @@ const ItemDetail: React.FC = () => {
|
||||
style={{ flex: "1 1 50%" }}
|
||||
/>
|
||||
<select
|
||||
className="form-select"
|
||||
className="form-select time-select"
|
||||
value={rentalDates.startTime}
|
||||
onChange={(e) =>
|
||||
handleDateTimeChange(
|
||||
@@ -661,8 +662,8 @@ const ItemDetail: React.FC = () => {
|
||||
</div>
|
||||
|
||||
<div className="mb-3">
|
||||
<label className="form-label small mb-1">End</label>
|
||||
<div className="input-group input-group-sm">
|
||||
<label className="form-label fw-medium mb-2">End</label>
|
||||
<div className="input-group input-group-lg">
|
||||
<input
|
||||
type="date"
|
||||
className="form-control"
|
||||
@@ -677,7 +678,7 @@ const ItemDetail: React.FC = () => {
|
||||
style={{ flex: "1 1 50%" }}
|
||||
/>
|
||||
<select
|
||||
className="form-select"
|
||||
className="form-select time-select"
|
||||
value={rentalDates.endTime}
|
||||
onChange={(e) =>
|
||||
handleDateTimeChange("endTime", e.target.value)
|
||||
@@ -787,6 +788,62 @@ const ItemDetail: React.FC = () => {
|
||||
reasonPlaceholder="Enter reason for deletion (e.g., policy violation, inappropriate content)"
|
||||
reasonRequired={true}
|
||||
/>
|
||||
|
||||
{/* Mobile Sticky Bottom Bar */}
|
||||
{!isOwner && item.isAvailable && (
|
||||
<div className="mobile-sticky-bottom-bar d-md-none">
|
||||
<div className="d-flex align-items-center justify-content-between">
|
||||
<div className="price-display">
|
||||
{(() => {
|
||||
const prices = [
|
||||
{ value: Number(item.pricePerHour), label: "/hour" },
|
||||
{ value: Number(item.pricePerDay), label: "/day" },
|
||||
{ value: Number(item.pricePerWeek), label: "/week" },
|
||||
{ value: Number(item.pricePerMonth), label: "/month" },
|
||||
].filter((p) => p.value > 0);
|
||||
|
||||
if (prices.length === 0) {
|
||||
return <span className="fw-bold">Free to Borrow</span>;
|
||||
}
|
||||
|
||||
const lowestPrice = prices[0];
|
||||
return (
|
||||
<span>
|
||||
<span className="fw-bold fs-5">
|
||||
${Math.floor(lowestPrice.value)}
|
||||
</span>
|
||||
<span className="text-muted">{lowestPrice.label}</span>
|
||||
</span>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
{isAlreadyRenting ? (
|
||||
<button
|
||||
className="btn btn-success"
|
||||
disabled
|
||||
style={{ opacity: 0.8 }}
|
||||
>
|
||||
Renting
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
className="btn btn-primary btn-lg"
|
||||
onClick={() => {
|
||||
const pricingCard = document.getElementById("pricing-card");
|
||||
if (pricingCard) {
|
||||
pricingCard.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "start",
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
Check Availability
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -32,6 +32,7 @@ const ItemList: React.FC = () => {
|
||||
if (locationCheckDone.current) return;
|
||||
|
||||
const hasLocation = searchParams.has("lat") || searchParams.has("city") || searchParams.has("zipCode");
|
||||
const hasSearchTerm = searchParams.has("search");
|
||||
|
||||
if (!hasLocation) {
|
||||
// Check user's saved address for lat/lng
|
||||
@@ -46,10 +47,13 @@ const ItemList: React.FC = () => {
|
||||
params.set("radius", "25");
|
||||
locationCheckDone.current = true;
|
||||
navigate(`/items?${params.toString()}`, { replace: true });
|
||||
} else {
|
||||
// No saved address with coordinates - show location prompt
|
||||
} else if (!hasSearchTerm) {
|
||||
// No saved address and no search term - show location prompt
|
||||
locationCheckDone.current = true;
|
||||
setShowLocationPrompt(true);
|
||||
} else {
|
||||
// Has search term but no location - just show results without location filter
|
||||
locationCheckDone.current = true;
|
||||
}
|
||||
} else {
|
||||
locationCheckDone.current = true;
|
||||
@@ -72,26 +76,15 @@ const ItemList: React.FC = () => {
|
||||
});
|
||||
}, [searchParams]);
|
||||
|
||||
const handleLocationSelect = (
|
||||
location: { lat: number; lng: number } | { city?: string; zipCode?: string }
|
||||
) => {
|
||||
const handleLocationSelect = (location: { lat: number; lng: number }) => {
|
||||
const params = new URLSearchParams(searchParams);
|
||||
|
||||
if ("lat" in location) {
|
||||
params.set("lat", location.lat.toString());
|
||||
params.set("lng", location.lng.toString());
|
||||
params.set("radius", "25");
|
||||
// Remove city/zipCode if using coordinates
|
||||
params.delete("city");
|
||||
params.delete("zipCode");
|
||||
} else {
|
||||
if (location.city) params.set("city", location.city);
|
||||
if (location.zipCode) params.set("zipCode", location.zipCode);
|
||||
// Remove lat/lng if using city/zip
|
||||
params.delete("lat");
|
||||
params.delete("lng");
|
||||
params.delete("radius");
|
||||
}
|
||||
params.set("lat", location.lat.toString());
|
||||
params.set("lng", location.lng.toString());
|
||||
params.set("radius", "25");
|
||||
// Remove city/zipCode since we're using coordinates
|
||||
params.delete("city");
|
||||
params.delete("zipCode");
|
||||
|
||||
navigate(`/items?${params.toString()}`, { replace: true });
|
||||
setShowLocationPrompt(false);
|
||||
|
||||
@@ -19,8 +19,6 @@ const RentItem: React.FC = () => {
|
||||
const [pendingSubmit, setPendingSubmit] = useState(false);
|
||||
|
||||
const [formData, setFormData] = useState({
|
||||
deliveryMethod: "pickup" as "pickup" | "delivery",
|
||||
deliveryAddress: "",
|
||||
intendedUse: "",
|
||||
});
|
||||
|
||||
@@ -146,8 +144,6 @@ const RentItem: React.FC = () => {
|
||||
itemId: id,
|
||||
startDateTime,
|
||||
endDateTime,
|
||||
deliveryMethod: formData.deliveryMethod,
|
||||
deliveryAddress: formData.deliveryAddress,
|
||||
intendedUse: formData.intendedUse || undefined,
|
||||
totalAmount: totalCost,
|
||||
};
|
||||
@@ -230,9 +226,9 @@ const RentItem: React.FC = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container mt-4">
|
||||
<div className="row">
|
||||
<div className="col-md-8">
|
||||
<div className="container mt-5">
|
||||
<div className="row justify-content-center">
|
||||
<div className="col-md-10">
|
||||
<h1>Renting {item.name}</h1>
|
||||
|
||||
{/* Email Verification Warning Banner */}
|
||||
@@ -259,6 +255,93 @@ const RentItem: React.FC = () => {
|
||||
)}
|
||||
|
||||
<div className="row">
|
||||
{/* Pricing Card - appears first on mobile, right side on desktop */}
|
||||
<div className="col-md-4 order-first order-md-last mb-4 mb-md-0">
|
||||
<div className="card">
|
||||
<div className="card-body">
|
||||
{item.imageFilenames && item.imageFilenames[0] && (
|
||||
<img
|
||||
src={getPublicImageUrl(item.imageFilenames[0])}
|
||||
alt={item.name}
|
||||
className="img-fluid rounded mb-3"
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "150px",
|
||||
objectFit: "contain",
|
||||
backgroundColor: "#f8f9fa",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<h6>{item.name}</h6>
|
||||
<p className="text-muted small">
|
||||
{item.city && item.state
|
||||
? `${item.city}, ${item.state}`
|
||||
: ""}
|
||||
</p>
|
||||
|
||||
<hr />
|
||||
|
||||
{/* Pricing */}
|
||||
<div className="mb-3 text-center">
|
||||
{totalCost === 0 ? (
|
||||
<h6>Free to Borrow</h6>
|
||||
) : (
|
||||
<>
|
||||
{item.pricePerHour && Number(item.pricePerHour) > 0 && (
|
||||
<h6>${Math.floor(Number(item.pricePerHour))}/Hour</h6>
|
||||
)}
|
||||
{item.pricePerDay && Number(item.pricePerDay) > 0 && (
|
||||
<h6>${Math.floor(Number(item.pricePerDay))}/Day</h6>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Selected Dates */}
|
||||
{manualSelection.startDate && manualSelection.endDate && (
|
||||
<div className="mb-3">
|
||||
<div className="small mb-1">
|
||||
<strong>Check-in:</strong>{" "}
|
||||
{formatDate(manualSelection.startDate)} at{" "}
|
||||
{formatTime(manualSelection.startTime)}
|
||||
</div>
|
||||
<div className="small">
|
||||
<strong>Check-out:</strong>{" "}
|
||||
{formatDate(manualSelection.endDate)} at{" "}
|
||||
{formatTime(manualSelection.endTime)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Total Cost */}
|
||||
<>
|
||||
<hr />
|
||||
<div className="d-flex justify-content-between">
|
||||
<strong>Total:</strong>
|
||||
{costLoading ? (
|
||||
<div
|
||||
className="spinner-border spinner-border-sm"
|
||||
role="status"
|
||||
>
|
||||
<span className="visually-hidden">
|
||||
Calculating...
|
||||
</span>
|
||||
</div>
|
||||
) : costError ? (
|
||||
<small className="text-danger">Error</small>
|
||||
) : totalCost > 0 ? (
|
||||
<strong>${totalCost}</strong>
|
||||
) : (
|
||||
<strong>$0</strong>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Form - appears second on mobile, left side on desktop */}
|
||||
<div className="col-md-8">
|
||||
{completed ? (
|
||||
<div className="card mb-4">
|
||||
@@ -377,91 +460,6 @@ const RentItem: React.FC = () => {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="col-md-4">
|
||||
<div className="card">
|
||||
<div className="card-body">
|
||||
{item.imageFilenames && item.imageFilenames[0] && (
|
||||
<img
|
||||
src={getPublicImageUrl(item.imageFilenames[0])}
|
||||
alt={item.name}
|
||||
className="img-fluid rounded mb-3"
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "150px",
|
||||
objectFit: "contain",
|
||||
backgroundColor: "#f8f9fa",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<h6>{item.name}</h6>
|
||||
<p className="text-muted small">
|
||||
{item.city && item.state
|
||||
? `${item.city}, ${item.state}`
|
||||
: ""}
|
||||
</p>
|
||||
|
||||
<hr />
|
||||
|
||||
{/* Pricing */}
|
||||
<div className="mb-3 text-center">
|
||||
{totalCost === 0 ? (
|
||||
<h6>Free to Borrow</h6>
|
||||
) : (
|
||||
<>
|
||||
{item.pricePerHour && Number(item.pricePerHour) > 0 && (
|
||||
<h6>${Math.floor(Number(item.pricePerHour))}/Hour</h6>
|
||||
)}
|
||||
{item.pricePerDay && Number(item.pricePerDay) > 0 && (
|
||||
<h6>${Math.floor(Number(item.pricePerDay))}/Day</h6>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Selected Dates */}
|
||||
{manualSelection.startDate && manualSelection.endDate && (
|
||||
<div className="mb-3">
|
||||
<div className="small mb-1">
|
||||
<strong>Check-in:</strong>{" "}
|
||||
{formatDate(manualSelection.startDate)} at{" "}
|
||||
{formatTime(manualSelection.startTime)}
|
||||
</div>
|
||||
<div className="small">
|
||||
<strong>Check-out:</strong>{" "}
|
||||
{formatDate(manualSelection.endDate)} at{" "}
|
||||
{formatTime(manualSelection.endTime)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Total Cost */}
|
||||
<>
|
||||
<hr />
|
||||
<div className="d-flex justify-content-between">
|
||||
<strong>Total:</strong>
|
||||
{costLoading ? (
|
||||
<div
|
||||
className="spinner-border spinner-border-sm"
|
||||
role="status"
|
||||
>
|
||||
<span className="visually-hidden">
|
||||
Calculating...
|
||||
</span>
|
||||
</div>
|
||||
) : costError ? (
|
||||
<small className="text-danger">Error</small>
|
||||
) : totalCost > 0 ? (
|
||||
<strong>${totalCost}</strong>
|
||||
) : (
|
||||
<strong>$0</strong>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user