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:
jackiettran
2025-12-30 00:20:15 -05:00
parent 7dd3aff0f8
commit 546c881701
12 changed files with 254 additions and 222 deletions

View File

@@ -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">

View File

@@ -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 || ""}

View File

@@ -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>
);
};

View File

@@ -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);

View File

@@ -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>