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

@@ -54,4 +54,61 @@ main {
.navbar .container-fluid {
justify-content: space-between;
}
}
/* Mobile Sticky Bottom Bar for Item Detail */
.mobile-sticky-bottom-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: white;
padding: 12px 16px;
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1);
z-index: 1000;
border-top: 1px solid #e0e0e0;
}
/* Mobile-specific styles */
@media (max-width: 767.98px) {
/* Make sticky card non-sticky on mobile */
.sticky-pricing-card {
position: static !important;
margin-bottom: 80px;
}
}
/* Pricing card input sizing - applies to all screen sizes */
.sticky-pricing-card .form-label {
font-size: 1rem;
}
.sticky-pricing-card .input-group-lg .form-control,
.sticky-pricing-card .input-group-lg .form-select {
font-size: 1rem;
padding: 12px 16px;
min-height: 48px;
}
/* Style the date input specifically */
.sticky-pricing-card input[type="date"] {
font-size: 1rem;
}
/* Make the calendar icon larger on webkit browsers */
.sticky-pricing-card input[type="date"]::-webkit-calendar-picker-indicator {
width: 20px;
height: 20px;
cursor: pointer;
}
/* Time select dropdown */
.sticky-pricing-card .time-select {
font-size: 1rem;
}
/* Style options in the time dropdown */
.sticky-pricing-card .time-select option {
font-size: 1rem;
padding: 8px;
}

View File

@@ -1,60 +0,0 @@
import React from 'react';
interface DeliveryOptionsProps {
pickUpAvailable: boolean;
inPlaceUseAvailable: boolean;
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
}
const DeliveryOptions: React.FC<DeliveryOptionsProps> = ({
pickUpAvailable,
inPlaceUseAvailable,
onChange
}) => {
return (
<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={pickUpAvailable}
onChange={onChange}
/>
<label className="form-check-label" htmlFor="pickUpAvailable">
Pick-Up/Drop-off
<div className="small text-muted">
You and the renter coordinate pick-up and drop-off
</div>
</label>
</div>
<div className="form-check">
<input
type="checkbox"
className="form-check-input"
id="inPlaceUseAvailable"
name="inPlaceUseAvailable"
checked={inPlaceUseAvailable}
onChange={onChange}
/>
<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>
);
};
export default DeliveryOptions;

View File

@@ -1,11 +1,10 @@
import React, { useState } from "react";
import { mapsAPI } from "../services/api";
interface LocationPromptModalProps {
show: boolean;
onClose: () => void;
onLocationSelect: (
location: { lat: number; lng: number } | { city?: string; zipCode?: string }
) => void;
onLocationSelect: (location: { lat: number; lng: number }) => void;
}
const LocationPromptModal: React.FC<LocationPromptModalProps> = ({
@@ -53,15 +52,31 @@ const LocationPromptModal: React.FC<LocationPromptModalProps> = ({
}
};
const handleManualSubmit = () => {
const handleManualSubmit = async () => {
const trimmed = manualLocation.trim();
if (!trimmed) return;
// Check if it looks like a ZIP code
if (/^\d{5}(-\d{4})?$/.test(trimmed)) {
onLocationSelect({ zipCode: trimmed });
} else {
onLocationSelect({ city: trimmed });
setLoading(true);
setError(null);
try {
// Geocode the input (works for both ZIP codes and city names)
const response = await mapsAPI.geocode({
address: trimmed,
componentRestrictions: { country: "US" },
});
const { latitude, longitude } = response.data;
if (latitude && longitude) {
onLocationSelect({ lat: latitude, lng: longitude });
} else {
setError("Could not find that location. Please try a different city or ZIP code.");
}
} catch (err: any) {
setError("Could not find that location. Please try a different city or ZIP code.");
} finally {
setLoading(false);
}
};

View File

@@ -1,3 +1,7 @@
html {
font-size: 20px;
}
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',

View File

@@ -48,7 +48,6 @@ export const mockRental = {
totalAmount: 25,
status: 'pending' as const,
paymentStatus: 'pending' as const,
deliveryMethod: 'pickup' as const,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};

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>

View File

@@ -66,11 +66,6 @@ export interface Item {
id: string;
name: string;
description: string;
pickUpAvailable?: boolean;
localDeliveryAvailable?: boolean;
localDeliveryRadius?: number;
shippingAvailable?: boolean;
inPlaceUseAvailable?: boolean;
pricePerHour?: number;
pricePerDay?: number;
pricePerWeek?: number;
@@ -147,8 +142,6 @@ export interface Rental {
payoutStatus?: "pending" | "completed" | "failed" | null;
payoutProcessedAt?: string;
stripeTransferId?: string;
deliveryMethod: "pickup" | "delivery";
deliveryAddress?: string;
intendedUse?: string;
rating?: number;
review?: string;