date time validation and added ability to type in date
This commit is contained in:
100
frontend/package-lock.json
generated
100
frontend/package-lock.json
generated
@@ -24,6 +24,7 @@
|
||||
"bootstrap": "^5.3.7",
|
||||
"browser-image-compression": "^2.0.2",
|
||||
"react": "^19.1.0",
|
||||
"react-datepicker": "^9.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-router-dom": "^6.30.1",
|
||||
"react-scripts": "5.0.1",
|
||||
@@ -2490,6 +2491,59 @@
|
||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/core": {
|
||||
"version": "1.7.3",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz",
|
||||
"integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/utils": "^0.2.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/dom": {
|
||||
"version": "1.7.4",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz",
|
||||
"integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/core": "^1.7.3",
|
||||
"@floating-ui/utils": "^0.2.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/react": {
|
||||
"version": "0.27.16",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.27.16.tgz",
|
||||
"integrity": "sha512-9O8N4SeG2z++TSM8QA/KTeKFBVCNEz/AGS7gWPJf6KFRzmRWixFRnCnkPHRDwSVZW6QPDO6uT0P2SpWNKCc9/g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/react-dom": "^2.1.6",
|
||||
"@floating-ui/utils": "^0.2.10",
|
||||
"tabbable": "^6.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=17.0.0",
|
||||
"react-dom": ">=17.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/react-dom": {
|
||||
"version": "2.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.6.tgz",
|
||||
"integrity": "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/dom": "^1.7.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/utils": {
|
||||
"version": "0.2.10",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz",
|
||||
"integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@googlemaps/js-api-loader": {
|
||||
"version": "1.16.10",
|
||||
"resolved": "https://registry.npmjs.org/@googlemaps/js-api-loader/-/js-api-loader-1.16.10.tgz",
|
||||
@@ -5920,6 +5974,15 @@
|
||||
"wrap-ansi": "^7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/clsx": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
|
||||
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/co": {
|
||||
"version": "4.6.0",
|
||||
"resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
|
||||
@@ -6731,6 +6794,16 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/date-fns": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
|
||||
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/kossnocorp"
|
||||
}
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
|
||||
@@ -14468,6 +14541,27 @@
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/react-datepicker": {
|
||||
"version": "9.1.0",
|
||||
"resolved": "https://registry.npmjs.org/react-datepicker/-/react-datepicker-9.1.0.tgz",
|
||||
"integrity": "sha512-lOp+m5bc+ttgtB5MHEjwiVu4nlp4CvJLS/PG1OiOe5pmg9kV73pEqO8H0Geqvg2E8gjqTaL9eRhSe+ZpeKP3nA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/react": "^0.27.15",
|
||||
"clsx": "^2.1.1",
|
||||
"date-fns": "^4.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"date-fns-tz": "^3.0.0",
|
||||
"react": "^16.9.0 || ^17 || ^18 || ^19 || ^19.0.0-rc",
|
||||
"react-dom": "^16.9.0 || ^17 || ^18 || ^19 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"date-fns-tz": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-dev-utils": {
|
||||
"version": "12.0.1",
|
||||
"resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.1.tgz",
|
||||
@@ -16658,6 +16752,12 @@
|
||||
"integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tabbable": {
|
||||
"version": "6.4.0",
|
||||
"resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.4.0.tgz",
|
||||
"integrity": "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tailwindcss": {
|
||||
"version": "3.4.17",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz",
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
"bootstrap": "^5.3.7",
|
||||
"browser-image-compression": "^2.0.2",
|
||||
"react": "^19.1.0",
|
||||
"react-datepicker": "^9.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-router-dom": "^6.30.1",
|
||||
"react-scripts": "5.0.1",
|
||||
|
||||
@@ -15,3 +15,82 @@ code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||
monospace;
|
||||
}
|
||||
|
||||
/* React DatePicker Customizations */
|
||||
.react-datepicker-wrapper {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.react-datepicker__input-container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.react-datepicker__input-container input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.react-datepicker {
|
||||
font-family: inherit;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 0.375rem;
|
||||
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.react-datepicker__header {
|
||||
background-color: #f8f9fa;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
.react-datepicker__day--selected,
|
||||
.react-datepicker__day--keyboard-selected,
|
||||
.react-datepicker__time-list-item--selected {
|
||||
background-color: #0d6efd !important;
|
||||
}
|
||||
|
||||
.react-datepicker__day:hover {
|
||||
background-color: #e9ecef;
|
||||
}
|
||||
|
||||
.react-datepicker__time-container {
|
||||
border-left: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
.react-datepicker__time-list-item:hover {
|
||||
background-color: #e9ecef !important;
|
||||
}
|
||||
|
||||
.react-datepicker__time-list-item--disabled {
|
||||
color: #adb5bd !important;
|
||||
}
|
||||
|
||||
.react-datepicker__navigation-icon::before {
|
||||
border-color: #6c757d;
|
||||
}
|
||||
|
||||
/* Ensure datepicker calendar is not clipped on mobile */
|
||||
.react-datepicker-popper {
|
||||
z-index: 1050 !important;
|
||||
}
|
||||
|
||||
.card {
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
/* Prevent calendar from going off-screen on mobile */
|
||||
@media (max-width: 767px) {
|
||||
.react-datepicker-popper {
|
||||
left: 0 !important;
|
||||
right: 0 !important;
|
||||
margin-left: auto !important;
|
||||
margin-right: auto !important;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.react-datepicker {
|
||||
max-width: 90vw;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useParams, useNavigate } from "react-router-dom";
|
||||
import DatePicker from "react-datepicker";
|
||||
import "react-datepicker/dist/react-datepicker.css";
|
||||
import { Item, Rental } from "../types";
|
||||
import { useAuth } from "../contexts/AuthContext";
|
||||
import { itemAPI, rentalAPI } from "../services/api";
|
||||
@@ -9,6 +11,60 @@ import ItemReviews from "../components/ItemReviews";
|
||||
import ConfirmationModal from "../components/ConfirmationModal";
|
||||
import Avatar from "../components/Avatar";
|
||||
|
||||
// Helper function to validate date selection
|
||||
const validateDates = (
|
||||
startDate: Date | null,
|
||||
startTime: string,
|
||||
endDate: Date | null,
|
||||
endTime: string
|
||||
): { isValid: boolean; error: string | null } => {
|
||||
if (!startDate || !endDate || !startTime || !endTime) {
|
||||
return { isValid: false, error: null }; // No error, just incomplete
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
||||
|
||||
// Check if start date is in the past
|
||||
const startDateOnly = new Date(
|
||||
startDate.getFullYear(),
|
||||
startDate.getMonth(),
|
||||
startDate.getDate()
|
||||
);
|
||||
if (startDateOnly < today) {
|
||||
return { isValid: false, error: "Start date cannot be in the past" };
|
||||
}
|
||||
|
||||
// Check if start datetime is in the past (for today's date)
|
||||
if (startDateOnly.getTime() === today.getTime()) {
|
||||
const [startHour] = startTime.split(":").map(Number);
|
||||
const startDateTime = new Date(startDate);
|
||||
startDateTime.setHours(startHour, 0, 0, 0);
|
||||
if (startDateTime < now) {
|
||||
return { isValid: false, error: "Start time cannot be in the past" };
|
||||
}
|
||||
}
|
||||
|
||||
// Check if end date is before start date
|
||||
const endDateOnly = new Date(
|
||||
endDate.getFullYear(),
|
||||
endDate.getMonth(),
|
||||
endDate.getDate()
|
||||
);
|
||||
if (endDateOnly < startDateOnly) {
|
||||
return { isValid: false, error: "End date must be on or after start date" };
|
||||
}
|
||||
|
||||
// Check if end time is after start time (same day scenario)
|
||||
if (startDateOnly.getTime() === endDateOnly.getTime()) {
|
||||
if (endTime <= startTime) {
|
||||
return { isValid: false, error: "End time must be after start time" };
|
||||
}
|
||||
}
|
||||
|
||||
return { isValid: true, error: null };
|
||||
};
|
||||
|
||||
const ItemDetail: React.FC = () => {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const navigate = useNavigate();
|
||||
@@ -18,12 +74,20 @@ const ItemDetail: React.FC = () => {
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [selectedImage, setSelectedImage] = useState(0);
|
||||
const [isAlreadyRenting, setIsAlreadyRenting] = useState(false);
|
||||
const [rentalDates, setRentalDates] = useState({
|
||||
startDate: "",
|
||||
startTime: "14:00",
|
||||
endDate: "",
|
||||
endTime: "12:00",
|
||||
const [rentalDates, setRentalDates] = useState<{
|
||||
startDate: Date | null;
|
||||
startTime: string;
|
||||
endDate: Date | null;
|
||||
endTime: string;
|
||||
}>({
|
||||
startDate: null,
|
||||
startTime: "",
|
||||
endDate: null,
|
||||
endTime: "",
|
||||
});
|
||||
const [dateValidationError, setDateValidationError] = useState<string | null>(
|
||||
null
|
||||
);
|
||||
const [totalCost, setTotalCost] = useState(0);
|
||||
const [costLoading, setCostLoading] = useState(false);
|
||||
const [costError, setCostError] = useState<string | null>(null);
|
||||
@@ -123,11 +187,18 @@ const ItemDetail: React.FC = () => {
|
||||
};
|
||||
|
||||
const handleRent = () => {
|
||||
const startDateTime = combineDateTimeToISO(
|
||||
rentalDates.startDate,
|
||||
rentalDates.startTime
|
||||
);
|
||||
const endDateTime = combineDateTimeToISO(
|
||||
rentalDates.endDate,
|
||||
rentalDates.endTime
|
||||
);
|
||||
if (!startDateTime || !endDateTime) return;
|
||||
const params = new URLSearchParams({
|
||||
startDate: rentalDates.startDate,
|
||||
startTime: rentalDates.startTime,
|
||||
endDate: rentalDates.endDate,
|
||||
endTime: rentalDates.endTime,
|
||||
startDateTime,
|
||||
endDateTime,
|
||||
});
|
||||
navigate(`/items/${id}/rent?${params.toString()}`);
|
||||
};
|
||||
@@ -142,15 +213,29 @@ const ItemDetail: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleDateTimeChange = (field: string, value: string) => {
|
||||
setRentalDates((prev) => ({
|
||||
...prev,
|
||||
[field]: value,
|
||||
}));
|
||||
// Helper to combine date and time into ISO string
|
||||
const combineDateTimeToISO = (
|
||||
date: Date | null,
|
||||
time: string
|
||||
): string | null => {
|
||||
if (!date || !time) return null;
|
||||
const [hours] = time.split(":").map(Number);
|
||||
const combined = new Date(date);
|
||||
combined.setHours(hours, 0, 0, 0);
|
||||
return combined.toISOString();
|
||||
};
|
||||
|
||||
const calculateTotalCost = async () => {
|
||||
if (!item || !rentalDates.startDate || !rentalDates.endDate) {
|
||||
const startDateTime = combineDateTimeToISO(
|
||||
rentalDates.startDate,
|
||||
rentalDates.startTime
|
||||
);
|
||||
const endDateTime = combineDateTimeToISO(
|
||||
rentalDates.endDate,
|
||||
rentalDates.endTime
|
||||
);
|
||||
|
||||
if (!item || !startDateTime || !endDateTime) {
|
||||
setTotalCost(0);
|
||||
return;
|
||||
}
|
||||
@@ -159,14 +244,6 @@ const ItemDetail: React.FC = () => {
|
||||
setCostLoading(true);
|
||||
setCostError(null);
|
||||
|
||||
const startDateTime = new Date(
|
||||
`${rentalDates.startDate}T${rentalDates.startTime}`
|
||||
).toISOString();
|
||||
|
||||
const endDateTime = new Date(
|
||||
`${rentalDates.endDate}T${rentalDates.endTime}`
|
||||
).toISOString();
|
||||
|
||||
const response = await rentalAPI.getRentalCostPreview({
|
||||
itemId: item.id,
|
||||
startDateTime,
|
||||
@@ -182,15 +259,17 @@ const ItemDetail: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const generateTimeOptions = (item: Item | null, selectedDate: string) => {
|
||||
const options = [];
|
||||
let availableAfter = "00:00";
|
||||
let availableBefore = "23:59";
|
||||
// Generate time options for dropdown based on item availability for a given date
|
||||
const generateTimeOptions = (
|
||||
selectedDate: Date | null
|
||||
): { value: string; label: string }[] => {
|
||||
const options: { value: string; label: string }[] = [];
|
||||
|
||||
// Determine time constraints only if we have both item and a valid selected date
|
||||
if (item && selectedDate && selectedDate.trim() !== "") {
|
||||
const date = new Date(selectedDate);
|
||||
const dayName = date
|
||||
let availableAfterHour = 0;
|
||||
let availableBeforeHour = 23;
|
||||
|
||||
if (item && selectedDate) {
|
||||
const dayName = selectedDate
|
||||
.toLocaleDateString("en-US", { weekday: "long" })
|
||||
.toLowerCase() as
|
||||
| "sunday"
|
||||
@@ -208,42 +287,30 @@ const ItemDetail: React.FC = () => {
|
||||
item.weeklyTimes[dayName]
|
||||
) {
|
||||
const dayTimes = item.weeklyTimes[dayName];
|
||||
availableAfter = dayTimes.availableAfter;
|
||||
availableBefore = dayTimes.availableBefore;
|
||||
availableAfterHour = parseInt(
|
||||
dayTimes.availableAfter.split(":")[0],
|
||||
10
|
||||
);
|
||||
availableBeforeHour = parseInt(
|
||||
dayTimes.availableBefore.split(":")[0],
|
||||
10
|
||||
);
|
||||
}
|
||||
// Otherwise use global times
|
||||
else if (item.availableAfter && item.availableBefore) {
|
||||
availableAfter = item.availableAfter;
|
||||
availableBefore = item.availableBefore;
|
||||
availableAfterHour = parseInt(item.availableAfter.split(":")[0], 10);
|
||||
availableBeforeHour = parseInt(item.availableBefore.split(":")[0], 10);
|
||||
}
|
||||
}
|
||||
|
||||
for (let hour = 0; hour < 24; hour++) {
|
||||
const time24 = `${hour.toString().padStart(2, "0")}:00`;
|
||||
|
||||
// Ensure consistent format for comparison (normalize to HH:MM)
|
||||
const normalizedAvailableAfter =
|
||||
availableAfter.length === 5 ? availableAfter : availableAfter + ":00";
|
||||
const normalizedAvailableBefore =
|
||||
availableBefore.length === 5
|
||||
? availableBefore
|
||||
: availableBefore + ":00";
|
||||
|
||||
// Check if this time is within the available range
|
||||
if (
|
||||
time24 >= normalizedAvailableAfter &&
|
||||
time24 <= normalizedAvailableBefore
|
||||
) {
|
||||
const hour12 = hour === 0 ? 12 : hour > 12 ? hour - 12 : hour;
|
||||
const period = hour < 12 ? "AM" : "PM";
|
||||
const time12 = `${hour12}:00 ${period}`;
|
||||
options.push({ value: time24, label: time12 });
|
||||
}
|
||||
}
|
||||
|
||||
// If no options are available, return at least one option to prevent empty dropdown
|
||||
if (options.length === 0) {
|
||||
options.push({ value: "00:00", label: "Not Available" });
|
||||
for (let hour = availableAfterHour; hour <= availableBeforeHour; hour++) {
|
||||
const hourStr = hour.toString().padStart(2, "0");
|
||||
const period = hour >= 12 ? "PM" : "AM";
|
||||
const displayHour = hour === 0 ? 12 : hour > 12 ? hour - 12 : hour;
|
||||
options.push({
|
||||
value: `${hourStr}:00`,
|
||||
label: `${displayHour}:00 ${period}`,
|
||||
});
|
||||
}
|
||||
|
||||
return options;
|
||||
@@ -253,44 +320,16 @@ const ItemDetail: React.FC = () => {
|
||||
calculateTotalCost();
|
||||
}, [rentalDates, item]);
|
||||
|
||||
// Validate and adjust selected times based on item availability
|
||||
// Validate dates whenever they change
|
||||
useEffect(() => {
|
||||
if (!item) return;
|
||||
|
||||
const validateAndAdjustTime = (date: string, currentTime: string) => {
|
||||
if (!date) return currentTime;
|
||||
|
||||
const availableOptions = generateTimeOptions(item, date);
|
||||
if (availableOptions.length === 0) return currentTime;
|
||||
|
||||
// If current time is not in available options, use the first available time
|
||||
const isCurrentTimeValid = availableOptions.some(
|
||||
(option) => option.value === currentTime
|
||||
);
|
||||
return isCurrentTimeValid ? currentTime : availableOptions[0].value;
|
||||
};
|
||||
|
||||
const adjustedStartTime = validateAndAdjustTime(
|
||||
const validation = validateDates(
|
||||
rentalDates.startDate,
|
||||
rentalDates.startTime
|
||||
);
|
||||
const adjustedEndTime = validateAndAdjustTime(
|
||||
rentalDates.endDate || rentalDates.startDate,
|
||||
rentalDates.startTime,
|
||||
rentalDates.endDate,
|
||||
rentalDates.endTime
|
||||
);
|
||||
|
||||
// Update state if times have changed
|
||||
if (
|
||||
adjustedStartTime !== rentalDates.startTime ||
|
||||
adjustedEndTime !== rentalDates.endTime
|
||||
) {
|
||||
setRentalDates((prev) => ({
|
||||
...prev,
|
||||
startTime: adjustedStartTime,
|
||||
endTime: adjustedEndTime,
|
||||
}));
|
||||
}
|
||||
}, [item, rentalDates.startDate, rentalDates.endDate]);
|
||||
setDateValidationError(validation.error);
|
||||
}, [rentalDates]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
@@ -419,15 +458,21 @@ const ItemDetail: React.FC = () => {
|
||||
{item.imageFilenames.length > 0 ? (
|
||||
<div className="mb-4">
|
||||
<img
|
||||
src={getImageUrl(item.imageFilenames[selectedImage], 'medium')}
|
||||
src={getImageUrl(
|
||||
item.imageFilenames[selectedImage],
|
||||
"medium"
|
||||
)}
|
||||
alt={item.name}
|
||||
className="img-fluid rounded mb-3"
|
||||
loading="lazy"
|
||||
onError={(e) => {
|
||||
const target = e.currentTarget;
|
||||
if (!target.dataset.fallback) {
|
||||
target.dataset.fallback = 'true';
|
||||
target.src = getImageUrl(item.imageFilenames[selectedImage], 'original');
|
||||
target.dataset.fallback = "true";
|
||||
target.src = getImageUrl(
|
||||
item.imageFilenames[selectedImage],
|
||||
"original"
|
||||
);
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
@@ -442,7 +487,7 @@ const ItemDetail: React.FC = () => {
|
||||
{item.imageFilenames.map((image, index) => (
|
||||
<img
|
||||
key={index}
|
||||
src={getImageUrl(image, 'thumbnail')}
|
||||
src={getImageUrl(image, "thumbnail")}
|
||||
alt={`${item.name} ${index + 1}`}
|
||||
className={`rounded cursor-pointer ${
|
||||
selectedImage === index
|
||||
@@ -453,8 +498,8 @@ const ItemDetail: React.FC = () => {
|
||||
onError={(e) => {
|
||||
const target = e.currentTarget;
|
||||
if (!target.dataset.fallback) {
|
||||
target.dataset.fallback = 'true';
|
||||
target.src = getImageUrl(image, 'original');
|
||||
target.dataset.fallback = "true";
|
||||
target.src = getImageUrl(image, "original");
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
@@ -605,7 +650,8 @@ const ItemDetail: React.FC = () => {
|
||||
Number(item.pricePerMonth) > 0 && (
|
||||
<div className="mb-2">
|
||||
<h4>
|
||||
${Math.floor(Number(item.pricePerMonth))}/Month
|
||||
${Math.floor(Number(item.pricePerMonth))}
|
||||
/Month
|
||||
</h4>
|
||||
</div>
|
||||
)}
|
||||
@@ -618,122 +664,136 @@ const ItemDetail: React.FC = () => {
|
||||
<>
|
||||
<hr />
|
||||
<div className="text-start">
|
||||
<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"
|
||||
value={rentalDates.startDate}
|
||||
onChange={(e) =>
|
||||
handleDateTimeChange(
|
||||
"startDate",
|
||||
e.target.value
|
||||
)
|
||||
}
|
||||
min={new Date().toLocaleDateString()}
|
||||
style={{ flex: "1 1 50%" }}
|
||||
/>
|
||||
<select
|
||||
className="form-select time-select"
|
||||
value={rentalDates.startTime}
|
||||
onChange={(e) =>
|
||||
handleDateTimeChange(
|
||||
"startTime",
|
||||
e.target.value
|
||||
)
|
||||
}
|
||||
style={{ flex: "1 1 50%" }}
|
||||
disabled={
|
||||
!!(
|
||||
rentalDates.startDate &&
|
||||
generateTimeOptions(
|
||||
item,
|
||||
rentalDates.startDate
|
||||
).every(
|
||||
(opt) => opt.label === "Not Available"
|
||||
)
|
||||
)
|
||||
}
|
||||
>
|
||||
{generateTimeOptions(
|
||||
item,
|
||||
rentalDates.startDate
|
||||
).map((option) => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-3">
|
||||
<label className="form-label fw-medium mb-2">End</label>
|
||||
<div className="input-group input-group-lg">
|
||||
<input
|
||||
type="date"
|
||||
className="form-control"
|
||||
value={rentalDates.endDate}
|
||||
onChange={(e) =>
|
||||
handleDateTimeChange("endDate", e.target.value)
|
||||
}
|
||||
min={
|
||||
rentalDates.startDate ||
|
||||
new Date().toLocaleDateString()
|
||||
}
|
||||
style={{ flex: "1 1 50%" }}
|
||||
/>
|
||||
<select
|
||||
className="form-select time-select"
|
||||
value={rentalDates.endTime}
|
||||
onChange={(e) =>
|
||||
handleDateTimeChange("endTime", e.target.value)
|
||||
}
|
||||
style={{ flex: "1 1 50%" }}
|
||||
disabled={
|
||||
!!(
|
||||
(rentalDates.endDate ||
|
||||
rentalDates.startDate) &&
|
||||
generateTimeOptions(
|
||||
item,
|
||||
rentalDates.endDate || rentalDates.startDate
|
||||
).every(
|
||||
(opt) => opt.label === "Not Available"
|
||||
)
|
||||
)
|
||||
}
|
||||
>
|
||||
{generateTimeOptions(
|
||||
item,
|
||||
rentalDates.endDate || rentalDates.startDate
|
||||
).map((option) => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{rentalDates.startDate && rentalDates.endDate && (
|
||||
<div className="mb-3 p-2 bg-light rounded text-center">
|
||||
{costLoading ? (
|
||||
<div
|
||||
className="spinner-border spinner-border-sm"
|
||||
role="status"
|
||||
>
|
||||
<span className="visually-hidden">
|
||||
Calculating...
|
||||
</span>
|
||||
</div>
|
||||
) : costError ? (
|
||||
<small className="text-danger">{costError}</small>
|
||||
) : totalCost > 0 ? (
|
||||
<strong>Total: ${totalCost}</strong>
|
||||
) : null}
|
||||
{dateValidationError && (
|
||||
<div
|
||||
className="alert alert-danger py-2 mb-3"
|
||||
role="alert"
|
||||
>
|
||||
<small>{dateValidationError}</small>
|
||||
</div>
|
||||
)}
|
||||
<div className="mb-3">
|
||||
<label className="form-label fw-medium mb-2">
|
||||
Start
|
||||
</label>
|
||||
<div className="d-flex gap-2">
|
||||
<div style={{ flex: "1 1 50%" }}>
|
||||
<DatePicker
|
||||
selected={rentalDates.startDate}
|
||||
onChange={(date: Date | null) =>
|
||||
setRentalDates((prev) => ({
|
||||
...prev,
|
||||
startDate: date,
|
||||
}))
|
||||
}
|
||||
minDate={new Date()}
|
||||
dateFormat="MM/dd/yyyy"
|
||||
placeholderText="mm/dd/yyyy"
|
||||
className="form-control form-control-lg w-100"
|
||||
popperProps={{ strategy: "fixed" }}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ flex: "1 1 50%" }}>
|
||||
<select
|
||||
className="form-select form-select-lg"
|
||||
value={rentalDates.startTime}
|
||||
onChange={(e) =>
|
||||
setRentalDates((prev) => ({
|
||||
...prev,
|
||||
startTime: e.target.value,
|
||||
}))
|
||||
}
|
||||
disabled={!rentalDates.startDate}
|
||||
>
|
||||
<option value="">Pickup</option>
|
||||
{generateTimeOptions(
|
||||
rentalDates.startDate
|
||||
).map((option) => (
|
||||
<option
|
||||
key={option.value}
|
||||
value={option.value}
|
||||
>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-3">
|
||||
<label className="form-label fw-medium mb-2">
|
||||
End
|
||||
</label>
|
||||
<div className="d-flex gap-2">
|
||||
<div style={{ flex: "1 1 50%" }}>
|
||||
<DatePicker
|
||||
selected={rentalDates.endDate}
|
||||
onChange={(date: Date | null) =>
|
||||
setRentalDates((prev) => ({
|
||||
...prev,
|
||||
endDate: date,
|
||||
}))
|
||||
}
|
||||
minDate={rentalDates.startDate || new Date()}
|
||||
dateFormat="MM/dd/yyyy"
|
||||
placeholderText="mm/dd/yyyy"
|
||||
className="form-control form-control-lg w-100"
|
||||
popperProps={{ strategy: "fixed" }}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ flex: "1 1 50%" }}>
|
||||
<select
|
||||
className="form-select form-select-lg"
|
||||
value={rentalDates.endTime}
|
||||
onChange={(e) =>
|
||||
setRentalDates((prev) => ({
|
||||
...prev,
|
||||
endTime: e.target.value,
|
||||
}))
|
||||
}
|
||||
disabled={!rentalDates.endDate}
|
||||
>
|
||||
<option value="">Return</option>
|
||||
{generateTimeOptions(rentalDates.endDate).map(
|
||||
(option) => (
|
||||
<option
|
||||
key={option.value}
|
||||
value={option.value}
|
||||
>
|
||||
{option.label}
|
||||
</option>
|
||||
)
|
||||
)}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{rentalDates.startDate &&
|
||||
rentalDates.startTime &&
|
||||
rentalDates.endDate &&
|
||||
rentalDates.endTime &&
|
||||
!dateValidationError && (
|
||||
<div className="mb-3 p-2 bg-light rounded text-center">
|
||||
{costLoading ? (
|
||||
<div
|
||||
className="spinner-border spinner-border-sm"
|
||||
role="status"
|
||||
>
|
||||
<span className="visually-hidden">
|
||||
Calculating...
|
||||
</span>
|
||||
</div>
|
||||
) : costError ? (
|
||||
<small className="text-danger">
|
||||
{costError}
|
||||
</small>
|
||||
) : totalCost > 0 ? (
|
||||
<strong>Total: ${totalCost}</strong>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
@@ -745,7 +805,11 @@ const ItemDetail: React.FC = () => {
|
||||
className="btn btn-primary"
|
||||
onClick={handleRent}
|
||||
disabled={
|
||||
!rentalDates.startDate || !rentalDates.endDate
|
||||
!rentalDates.startDate ||
|
||||
!rentalDates.startTime ||
|
||||
!rentalDates.endDate ||
|
||||
!rentalDates.endTime ||
|
||||
!!dateValidationError
|
||||
}
|
||||
>
|
||||
Rent Now
|
||||
@@ -759,7 +823,7 @@ const ItemDetail: React.FC = () => {
|
||||
disabled
|
||||
style={{ opacity: 0.8 }}
|
||||
>
|
||||
✓ Renting
|
||||
Renting
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
@@ -852,122 +916,134 @@ const ItemDetail: React.FC = () => {
|
||||
<>
|
||||
<hr />
|
||||
<div className="text-start">
|
||||
<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"
|
||||
value={rentalDates.startDate}
|
||||
onChange={(e) =>
|
||||
handleDateTimeChange(
|
||||
"startDate",
|
||||
e.target.value
|
||||
)
|
||||
}
|
||||
min={new Date().toLocaleDateString()}
|
||||
style={{ flex: "1 1 50%" }}
|
||||
/>
|
||||
<select
|
||||
className="form-select time-select"
|
||||
value={rentalDates.startTime}
|
||||
onChange={(e) =>
|
||||
handleDateTimeChange(
|
||||
"startTime",
|
||||
e.target.value
|
||||
)
|
||||
}
|
||||
style={{ flex: "1 1 50%" }}
|
||||
disabled={
|
||||
!!(
|
||||
rentalDates.startDate &&
|
||||
generateTimeOptions(
|
||||
item,
|
||||
rentalDates.startDate
|
||||
).every(
|
||||
(opt) => opt.label === "Not Available"
|
||||
)
|
||||
)
|
||||
}
|
||||
>
|
||||
{generateTimeOptions(
|
||||
item,
|
||||
rentalDates.startDate
|
||||
).map((option) => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-3">
|
||||
<label className="form-label fw-medium mb-2">End</label>
|
||||
<div className="input-group input-group-lg">
|
||||
<input
|
||||
type="date"
|
||||
className="form-control"
|
||||
value={rentalDates.endDate}
|
||||
onChange={(e) =>
|
||||
handleDateTimeChange("endDate", e.target.value)
|
||||
}
|
||||
min={
|
||||
rentalDates.startDate ||
|
||||
new Date().toLocaleDateString()
|
||||
}
|
||||
style={{ flex: "1 1 50%" }}
|
||||
/>
|
||||
<select
|
||||
className="form-select time-select"
|
||||
value={rentalDates.endTime}
|
||||
onChange={(e) =>
|
||||
handleDateTimeChange("endTime", e.target.value)
|
||||
}
|
||||
style={{ flex: "1 1 50%" }}
|
||||
disabled={
|
||||
!!(
|
||||
(rentalDates.endDate ||
|
||||
rentalDates.startDate) &&
|
||||
generateTimeOptions(
|
||||
item,
|
||||
rentalDates.endDate || rentalDates.startDate
|
||||
).every(
|
||||
(opt) => opt.label === "Not Available"
|
||||
)
|
||||
)
|
||||
}
|
||||
>
|
||||
{generateTimeOptions(
|
||||
item,
|
||||
rentalDates.endDate || rentalDates.startDate
|
||||
).map((option) => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{rentalDates.startDate && rentalDates.endDate && (
|
||||
<div className="mb-3 p-2 bg-light rounded text-center">
|
||||
{costLoading ? (
|
||||
<div
|
||||
className="spinner-border spinner-border-sm"
|
||||
role="status"
|
||||
>
|
||||
<span className="visually-hidden">
|
||||
Calculating...
|
||||
</span>
|
||||
</div>
|
||||
) : costError ? (
|
||||
<small className="text-danger">{costError}</small>
|
||||
) : totalCost > 0 ? (
|
||||
<strong>Total: ${totalCost}</strong>
|
||||
) : null}
|
||||
{dateValidationError && (
|
||||
<div
|
||||
className="alert alert-danger py-2 mb-3"
|
||||
role="alert"
|
||||
>
|
||||
<small>{dateValidationError}</small>
|
||||
</div>
|
||||
)}
|
||||
<div className="mb-3">
|
||||
<label className="form-label fw-medium mb-2">
|
||||
Start
|
||||
</label>
|
||||
<div className="d-flex gap-2">
|
||||
<div style={{ flex: "1 1 50%" }}>
|
||||
<DatePicker
|
||||
selected={rentalDates.startDate}
|
||||
onChange={(date: Date | null) =>
|
||||
setRentalDates((prev) => ({
|
||||
...prev,
|
||||
startDate: date,
|
||||
}))
|
||||
}
|
||||
minDate={new Date()}
|
||||
dateFormat="MM/dd/yyyy"
|
||||
placeholderText="mm/dd/yyyy"
|
||||
className="form-control w-100"
|
||||
/>
|
||||
</div>
|
||||
<div style={{ flex: "1 1 50%" }}>
|
||||
<select
|
||||
className="form-select"
|
||||
value={rentalDates.startTime}
|
||||
onChange={(e) =>
|
||||
setRentalDates((prev) => ({
|
||||
...prev,
|
||||
startTime: e.target.value,
|
||||
}))
|
||||
}
|
||||
disabled={!rentalDates.startDate}
|
||||
>
|
||||
<option value="">Pickup</option>
|
||||
{generateTimeOptions(rentalDates.startDate).map(
|
||||
(option) => (
|
||||
<option
|
||||
key={option.value}
|
||||
value={option.value}
|
||||
>
|
||||
{option.label}
|
||||
</option>
|
||||
)
|
||||
)}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-3">
|
||||
<label className="form-label fw-medium mb-2">
|
||||
End
|
||||
</label>
|
||||
<div className="d-flex gap-2">
|
||||
<div style={{ flex: "1 1 50%" }}>
|
||||
<DatePicker
|
||||
selected={rentalDates.endDate}
|
||||
onChange={(date: Date | null) =>
|
||||
setRentalDates((prev) => ({
|
||||
...prev,
|
||||
endDate: date,
|
||||
}))
|
||||
}
|
||||
minDate={rentalDates.startDate || new Date()}
|
||||
dateFormat="MM/dd/yyyy"
|
||||
placeholderText="mm/dd/yyyy"
|
||||
className="form-control w-100"
|
||||
/>
|
||||
</div>
|
||||
<div style={{ flex: "1 1 50%" }}>
|
||||
<select
|
||||
className="form-select"
|
||||
value={rentalDates.endTime}
|
||||
onChange={(e) =>
|
||||
setRentalDates((prev) => ({
|
||||
...prev,
|
||||
endTime: e.target.value,
|
||||
}))
|
||||
}
|
||||
disabled={!rentalDates.endDate}
|
||||
>
|
||||
<option value="">Return</option>
|
||||
{generateTimeOptions(rentalDates.endDate).map(
|
||||
(option) => (
|
||||
<option
|
||||
key={option.value}
|
||||
value={option.value}
|
||||
>
|
||||
{option.label}
|
||||
</option>
|
||||
)
|
||||
)}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{rentalDates.startDate &&
|
||||
rentalDates.startTime &&
|
||||
rentalDates.endDate &&
|
||||
rentalDates.endTime &&
|
||||
!dateValidationError && (
|
||||
<div className="mb-3 p-2 bg-light rounded text-center">
|
||||
{costLoading ? (
|
||||
<div
|
||||
className="spinner-border spinner-border-sm"
|
||||
role="status"
|
||||
>
|
||||
<span className="visually-hidden">
|
||||
Calculating...
|
||||
</span>
|
||||
</div>
|
||||
) : costError ? (
|
||||
<small className="text-danger">
|
||||
{costError}
|
||||
</small>
|
||||
) : totalCost > 0 ? (
|
||||
<strong>Total: ${totalCost}</strong>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
@@ -979,7 +1055,11 @@ const ItemDetail: React.FC = () => {
|
||||
className="btn btn-primary"
|
||||
onClick={handleRent}
|
||||
disabled={
|
||||
!rentalDates.startDate || !rentalDates.endDate
|
||||
!rentalDates.startDate ||
|
||||
!rentalDates.startTime ||
|
||||
!rentalDates.endDate ||
|
||||
!rentalDates.endTime ||
|
||||
!!dateValidationError
|
||||
}
|
||||
>
|
||||
Rent Now
|
||||
@@ -993,7 +1073,7 @@ const ItemDetail: React.FC = () => {
|
||||
disabled
|
||||
style={{ opacity: 0.8 }}
|
||||
>
|
||||
✓ Renting
|
||||
Renting
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useParams, useNavigate, useSearchParams } from "react-router-dom";
|
||||
import { Item } from "../types";
|
||||
import { useAuth } from "../contexts/AuthContext";
|
||||
@@ -7,6 +7,37 @@ import { getImageUrl } from "../services/uploadService";
|
||||
import EmbeddedStripeCheckout from "../components/EmbeddedStripeCheckout";
|
||||
import VerificationCodeModal from "../components/VerificationCodeModal";
|
||||
|
||||
// Helper function to validate date selection
|
||||
const validateDates = (
|
||||
startDateTime: Date | null,
|
||||
endDateTime: Date | null
|
||||
): { isValid: boolean; error: string | null } => {
|
||||
if (!startDateTime || !endDateTime) {
|
||||
return { isValid: false, error: "Start and end date/time are required" };
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
|
||||
// Check if start datetime is in the past
|
||||
if (startDateTime < now) {
|
||||
return { isValid: false, error: "Start date/time cannot be in the past" };
|
||||
}
|
||||
|
||||
// Check if end datetime is after start datetime
|
||||
if (endDateTime <= startDateTime) {
|
||||
return { isValid: false, error: "End date/time must be after start date/time" };
|
||||
}
|
||||
|
||||
return { isValid: true, error: null };
|
||||
};
|
||||
|
||||
// Helper to parse ISO string from URL params
|
||||
const parseDateTime = (isoString: string | null): Date | null => {
|
||||
if (!isoString) return null;
|
||||
const date = new Date(isoString);
|
||||
return isNaN(date.getTime()) ? null : date;
|
||||
};
|
||||
|
||||
const RentItem: React.FC = () => {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const navigate = useNavigate();
|
||||
@@ -22,53 +53,34 @@ const RentItem: React.FC = () => {
|
||||
intendedUse: "",
|
||||
});
|
||||
|
||||
const [manualSelection, setManualSelection] = useState({
|
||||
startDate: searchParams.get("startDate") || "",
|
||||
startTime: searchParams.get("startTime") || "09:00",
|
||||
endDate: searchParams.get("endDate") || "",
|
||||
endTime: searchParams.get("endTime") || "17:00",
|
||||
const [rentalDates, setRentalDates] = useState<{
|
||||
startDateTime: Date | null;
|
||||
endDateTime: Date | null;
|
||||
}>({
|
||||
startDateTime: parseDateTime(searchParams.get("startDateTime")),
|
||||
endDateTime: parseDateTime(searchParams.get("endDateTime")),
|
||||
});
|
||||
|
||||
const [totalCost, setTotalCost] = useState(0);
|
||||
const [costLoading, setCostLoading] = useState(false);
|
||||
const [costError, setCostError] = useState<string | null>(null);
|
||||
const [completed, setCompleted] = useState(false);
|
||||
const [dateValidationError, setDateValidationError] = useState<string | null>(null);
|
||||
|
||||
const convertToUTC = (dateString: string, timeString: string): string => {
|
||||
if (!dateString || !timeString) {
|
||||
throw new Error("Date and time are required");
|
||||
}
|
||||
|
||||
// Create date in user's local timezone
|
||||
const localDateTime = new Date(`${dateString}T${timeString}`);
|
||||
|
||||
// Return UTC ISO string
|
||||
return localDateTime.toISOString();
|
||||
};
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
if (!dateString) return "";
|
||||
// Use safe date parsing to avoid timezone issues
|
||||
const [year, month, day] = dateString.split("-");
|
||||
const date = new Date(parseInt(year), parseInt(month) - 1, parseInt(day));
|
||||
return date.toLocaleDateString();
|
||||
};
|
||||
|
||||
const formatTime = (timeString: string) => {
|
||||
if (!timeString) return "";
|
||||
const [hour, minute] = timeString.split(":");
|
||||
const hour12 =
|
||||
parseInt(hour) === 0
|
||||
? 12
|
||||
: parseInt(hour) > 12
|
||||
? parseInt(hour) - 12
|
||||
: parseInt(hour);
|
||||
const period = parseInt(hour) < 12 ? "AM" : "PM";
|
||||
return `${hour12}:${minute} ${period}`;
|
||||
const formatDateTime = (date: Date | null) => {
|
||||
if (!date) return "";
|
||||
return date.toLocaleString("en-US", {
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
year: "numeric",
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
hour12: true,
|
||||
});
|
||||
};
|
||||
|
||||
const calculateTotalCost = async () => {
|
||||
if (!item || !manualSelection.startDate || !manualSelection.endDate) {
|
||||
if (!item || !rentalDates.startDateTime || !rentalDates.endDateTime) {
|
||||
setTotalCost(0);
|
||||
return;
|
||||
}
|
||||
@@ -77,18 +89,10 @@ const RentItem: React.FC = () => {
|
||||
setCostLoading(true);
|
||||
setCostError(null);
|
||||
|
||||
const startDateTime = new Date(
|
||||
`${manualSelection.startDate}T${manualSelection.startTime}`
|
||||
).toISOString();
|
||||
|
||||
const endDateTime = new Date(
|
||||
`${manualSelection.endDate}T${manualSelection.endTime}`
|
||||
).toISOString();
|
||||
|
||||
const response = await rentalAPI.getRentalCostPreview({
|
||||
itemId: item.id,
|
||||
startDateTime,
|
||||
endDateTime,
|
||||
startDateTime: rentalDates.startDateTime.toISOString(),
|
||||
endDateTime: rentalDates.endDateTime.toISOString(),
|
||||
});
|
||||
|
||||
setTotalCost(response.data.baseAmount);
|
||||
@@ -106,7 +110,16 @@ const RentItem: React.FC = () => {
|
||||
|
||||
useEffect(() => {
|
||||
calculateTotalCost();
|
||||
}, [item, manualSelection]);
|
||||
}, [item, rentalDates]);
|
||||
|
||||
// Validate dates whenever they change
|
||||
useEffect(() => {
|
||||
const validation = validateDates(
|
||||
rentalDates.startDateTime,
|
||||
rentalDates.endDateTime
|
||||
);
|
||||
setDateValidationError(validation.error);
|
||||
}, [rentalDates]);
|
||||
|
||||
const fetchItem = async () => {
|
||||
try {
|
||||
@@ -130,27 +143,17 @@ const RentItem: React.FC = () => {
|
||||
};
|
||||
|
||||
const getRentalData = () => {
|
||||
try {
|
||||
const startDateTime = convertToUTC(
|
||||
manualSelection.startDate,
|
||||
manualSelection.startTime
|
||||
);
|
||||
const endDateTime = convertToUTC(
|
||||
manualSelection.endDate,
|
||||
manualSelection.endTime
|
||||
);
|
||||
|
||||
return {
|
||||
itemId: id,
|
||||
startDateTime,
|
||||
endDateTime,
|
||||
intendedUse: formData.intendedUse || undefined,
|
||||
totalAmount: totalCost,
|
||||
};
|
||||
} catch (error: any) {
|
||||
setError(error.message);
|
||||
if (!rentalDates.startDateTime || !rentalDates.endDateTime) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
itemId: id,
|
||||
startDateTime: rentalDates.startDateTime.toISOString(),
|
||||
endDateTime: rentalDates.endDateTime.toISOString(),
|
||||
intendedUse: formData.intendedUse || undefined,
|
||||
totalAmount: totalCost,
|
||||
};
|
||||
};
|
||||
|
||||
const handleFreeBorrow = async () => {
|
||||
@@ -306,17 +309,15 @@ const RentItem: React.FC = () => {
|
||||
</div>
|
||||
|
||||
{/* Selected Dates */}
|
||||
{manualSelection.startDate && manualSelection.endDate && (
|
||||
{rentalDates.startDateTime && rentalDates.endDateTime && (
|
||||
<div className="mb-3">
|
||||
<div className="small mb-1">
|
||||
<strong>Check-in:</strong>{" "}
|
||||
{formatDate(manualSelection.startDate)} at{" "}
|
||||
{formatTime(manualSelection.startTime)}
|
||||
{formatDateTime(rentalDates.startDateTime)}
|
||||
</div>
|
||||
<div className="small">
|
||||
<strong>Check-out:</strong>{" "}
|
||||
{formatDate(manualSelection.endDate)} at{" "}
|
||||
{formatTime(manualSelection.endTime)}
|
||||
{formatDateTime(rentalDates.endDateTime)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -413,8 +414,13 @@ const RentItem: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!manualSelection.startDate ||
|
||||
!manualSelection.endDate ||
|
||||
{dateValidationError ? (
|
||||
<div className="alert alert-danger">
|
||||
<i className="bi bi-exclamation-triangle me-2"></i>
|
||||
{dateValidationError}
|
||||
</div>
|
||||
) : !rentalDates.startDateTime ||
|
||||
!rentalDates.endDateTime ||
|
||||
!getRentalData() ? (
|
||||
<div className="alert alert-info">
|
||||
<i className="bi bi-info-circle me-2"></i>
|
||||
|
||||
Reference in New Issue
Block a user