From f289022b5d1c1b944afd32ba25aca47a88db137f Mon Sep 17 00:00:00 2001 From: jackiettran <41605212+jackiettran@users.noreply.github.com> Date: Fri, 18 Jul 2025 00:31:23 -0400 Subject: [PATCH] date selection --- .../src/components/AvailabilityCalendar.tsx | 840 +++++++++++++----- frontend/src/components/PrivateRoute.tsx | 5 +- frontend/src/pages/CreateItem.tsx | 2 +- frontend/src/pages/EditItem.tsx | 2 +- frontend/src/pages/ItemDetail.tsx | 5 +- frontend/src/pages/Login.tsx | 9 +- frontend/src/pages/Register.tsx | 7 +- frontend/src/pages/RentItem.tsx | 223 ++++- frontend/src/services/api.ts | 12 +- 9 files changed, 852 insertions(+), 253 deletions(-) diff --git a/frontend/src/components/AvailabilityCalendar.tsx b/frontend/src/components/AvailabilityCalendar.tsx index c0f2548..e817303 100644 --- a/frontend/src/components/AvailabilityCalendar.tsx +++ b/frontend/src/components/AvailabilityCalendar.tsx @@ -13,8 +13,9 @@ interface UnavailablePeriod { interface AvailabilityCalendarProps { unavailablePeriods: UnavailablePeriod[]; onPeriodsChange: (periods: UnavailablePeriod[]) => void; - priceType?: 'hour' | 'day'; - isRentalMode?: boolean; + mode: 'owner' | 'renter'; // Clear mode distinction + onRentalSelect?: (period: { startDate: Date; endDate: Date; startTime?: string; endTime?: string }) => void; + selectedRentalPeriod?: UnavailablePeriod; // External selection to display } type ViewType = 'month' | 'week' | 'day'; @@ -22,19 +23,20 @@ type ViewType = 'month' | 'week' | 'day'; const AvailabilityCalendar: React.FC = ({ unavailablePeriods, onPeriodsChange, - priceType = 'hour', - isRentalMode = false + mode, + onRentalSelect, + selectedRentalPeriod: externalSelectedPeriod }) => { const [currentDate, setCurrentDate] = useState(new Date()); const [viewType, setViewType] = useState('month'); const [selectionStart, setSelectionStart] = useState(null); - - // Reset to month view if priceType is day and current view is week/day - useEffect(() => { - if (priceType === 'day' && (viewType === 'week' || viewType === 'day')) { - setViewType('month'); - } - }, [priceType]); + const [selectionStartHour, setSelectionStartHour] = useState(null); + const [internalSelectedPeriod, setInternalSelectedPeriod] = useState(null); + const [ownerSelectionStart, setOwnerSelectionStart] = useState(null); + const [ownerSelectionStartHour, setOwnerSelectionStartHour] = useState(null); + + // Use external selection if provided, otherwise use internal + const selectedRentalPeriod = externalSelectedPeriod || internalSelectedPeriod; const getDaysInMonth = (date: Date) => { const year = date.getFullYear(); @@ -77,10 +79,14 @@ const AvailabilityCalendar: React.FC = ({ const isDateInPeriod = (date: Date, period: UnavailablePeriod) => { const start = new Date(period.startDate); const end = new Date(period.endDate); - start.setHours(0, 0, 0, 0); - end.setHours(23, 59, 59, 999); const checkDate = new Date(date); + + // Normalize all dates to midnight for comparison + start.setHours(0, 0, 0, 0); + end.setHours(0, 0, 0, 0); checkDate.setHours(0, 0, 0, 0); + + // Basic date range check return checkDate >= start && checkDate <= end; }; @@ -158,157 +164,400 @@ const AvailabilityCalendar: React.FC = ({ }); }; - const handleDateClick = (date: Date) => { - // Check if this date has an accepted rental - const hasAcceptedRental = unavailablePeriods.some(p => - p.isAcceptedRental && isDateInPeriod(date, p) - ); + const handleHourClick = (date: Date, hour: number) => { + if (mode !== 'renter') return; - if (hasAcceptedRental) { - // Don't allow clicking on accepted rental dates - return; - } + // Check if this hour is unavailable + if (isHourUnavailable(date, hour)) return; - if (!isRentalMode) { - toggleDateAvailability(date); - return; - } - - // Check if clicking on an existing rental selection to clear it - const existingRental = unavailablePeriods.find(p => - p.isRentalSelection && isDateInPeriod(date, p) - ); - - if (existingRental) { - // Clear the rental selection - onPeriodsChange(unavailablePeriods.filter(p => p.id !== existingRental.id)); - setSelectionStart(null); - return; - } - - // Two-click selection in rental mode - if (!selectionStart) { - // First click - set start date + if (!selectionStart || !selectionStartHour) { + // First click - set start date and hour setSelectionStart(date); + setSelectionStartHour(hour); + setInternalSelectedPeriod(null); } else { - // Second click - create rental period - const start = new Date(Math.min(selectionStart.getTime(), date.getTime())); - const end = new Date(Math.max(selectionStart.getTime(), date.getTime())); + // Second click - complete selection + const startDate = new Date(selectionStart); + const endDate = new Date(date); - // Check if any date in range is unavailable - let currentDate = new Date(start); + // Determine which date/hour comes first + let finalStart, finalEnd, startHour, endHour; + + if (startDate.toDateString() === endDate.toDateString()) { + // Same day - compare hours + if (selectionStartHour <= hour) { + finalStart = startDate; + finalEnd = endDate; + startHour = selectionStartHour; + endHour = hour; + } else { + finalStart = endDate; + finalEnd = startDate; + startHour = hour; + endHour = selectionStartHour; + } + } else if (startDate < endDate) { + finalStart = startDate; + finalEnd = endDate; + startHour = selectionStartHour; + endHour = hour; + } else { + finalStart = endDate; + finalEnd = startDate; + startHour = hour; + endHour = selectionStartHour; + } + + // Set the hours + finalStart.setHours(startHour, 0, 0, 0); + finalEnd.setHours(endHour, 59, 59, 999); + + // Check if any hour in range is unavailable + let current = new Date(finalStart); let hasUnavailable = false; - while (currentDate <= end) { - if (unavailablePeriods.some(p => !p.isRentalSelection && isDateInPeriod(currentDate, p))) { + + while (current <= finalEnd) { + const currentHour = current.getHours(); + if (isHourUnavailable(current, currentHour)) { hasUnavailable = true; break; } - currentDate.setDate(currentDate.getDate() + 1); + current.setHours(current.getHours() + 1); } if (hasUnavailable) { - // Range contains unavailable dates, reset selection + // Range contains unavailable hours, reset setSelectionStart(null); + setSelectionStartHour(null); return; } - // Clear existing rental selections and add new one - const nonRentalPeriods = unavailablePeriods.filter(p => !p.isRentalSelection); - const newPeriod: UnavailablePeriod = { - id: Date.now().toString(), - startDate: start, - endDate: end, + // Create rental selection + const rentalPeriod: UnavailablePeriod = { + id: 'rental-selection', + startDate: finalStart, + endDate: finalEnd, + startTime: `${startHour.toString().padStart(2, '0')}:00`, + endTime: `${endHour.toString().padStart(2, '0')}:00`, isRentalSelection: true }; - onPeriodsChange([...nonRentalPeriods, newPeriod]); - // Reset selection + setInternalSelectedPeriod(rentalPeriod); setSelectionStart(null); + setSelectionStartHour(null); + + // Notify parent + if (onRentalSelect) { + onRentalSelect({ + startDate: finalStart, + endDate: finalEnd, + startTime: `${startHour.toString().padStart(2, '0')}:00`, + endTime: `${endHour.toString().padStart(2, '0')}:00` + }); + } } }; - const toggleDateAvailability = (date: Date) => { - const dateStr = date.toISOString().split('T')[0]; - - if (isRentalMode) { - // In rental mode, only handle rental selections (green), not unavailable periods (red) - const existingRentalPeriod = unavailablePeriods.find(period => { - const periodStart = new Date(period.startDate).toISOString().split('T')[0]; - const periodEnd = new Date(period.endDate).toISOString().split('T')[0]; - return period.isRentalSelection && periodStart === dateStr && periodEnd === dateStr && !period.startTime && !period.endTime; - }); - - // Check if date is already unavailable (not a rental selection) - const isUnavailable = unavailablePeriods.some(p => - !p.isRentalSelection && isDateInPeriod(date, p) + const handleDateClick = (date: Date) => { + if (mode === 'owner') { + // OWNER MODE: Toggle unavailability for dates + // Check if this date has an accepted rental - can't modify those + const hasAcceptedRental = unavailablePeriods.some(p => + p.isAcceptedRental && isDateInPeriod(date, p) ); - if (isUnavailable) { - // Can't select unavailable dates + if (hasAcceptedRental) { + return; // Can't modify dates with accepted rentals + } + + // Check if this date is part of any unavailable period + const isUnavailable = isDateUnavailable(date); + + if (isUnavailable && !ownerSelectionStart) { + // Single click on an unavailable date - split periods to make just this date available + const updatedPeriods: UnavailablePeriod[] = []; + + unavailablePeriods.forEach(period => { + if (period.isAcceptedRental || !isDateInPeriod(date, period)) { + // Keep accepted rentals and periods that don't contain this date + updatedPeriods.push(period); + } else { + // Split the period around this date + const periodStart = new Date(period.startDate); + const periodEnd = new Date(period.endDate); + const targetDate = new Date(date); + + periodStart.setHours(0, 0, 0, 0); + periodEnd.setHours(0, 0, 0, 0); + targetDate.setHours(0, 0, 0, 0); + + // If there are dates before the target date, create a period for them + if (periodStart < targetDate) { + const beforeDate = new Date(targetDate); + beforeDate.setDate(beforeDate.getDate() - 1); + updatedPeriods.push({ + id: Date.now().toString() + '-before', + startDate: periodStart, + endDate: beforeDate, + startTime: period.startTime, + endTime: period.endTime + }); + } + + // If there are dates after the target date, create a period for them + if (periodEnd > targetDate) { + const afterDate = new Date(targetDate); + afterDate.setDate(afterDate.getDate() + 1); + updatedPeriods.push({ + id: Date.now().toString() + '-after', + startDate: afterDate, + endDate: periodEnd, + startTime: period.startTime, + endTime: period.endTime + }); + } + } + }); + + onPeriodsChange(updatedPeriods); + setOwnerSelectionStart(null); + } else if (!ownerSelectionStart) { + // First click - start selection + setOwnerSelectionStart(date); + } else { + // Second click - complete range selection + const start = new Date(Math.min(ownerSelectionStart.getTime(), date.getTime())); + const end = new Date(Math.max(ownerSelectionStart.getTime(), date.getTime())); + + // Check if any date in range has accepted rental + let current = new Date(start); + let hasAcceptedInRange = false; + while (current <= end) { + if (unavailablePeriods.some(p => p.isAcceptedRental && isDateInPeriod(current, p))) { + hasAcceptedInRange = true; + break; + } + current.setDate(current.getDate() + 1); + } + + if (hasAcceptedInRange) { + // Can't select range with accepted rentals + setOwnerSelectionStart(null); + return; + } + + // Add new unavailable period for the range + const newPeriod: UnavailablePeriod = { + id: Date.now().toString(), + startDate: start, + endDate: end + }; + onPeriodsChange([...unavailablePeriods, newPeriod]); + setOwnerSelectionStart(null); + } + } else { + // RENTER MODE: Select rental period + // Check if date is unavailable + if (isDateUnavailable(date)) { + return; // Can't select unavailable dates + } + + if (!selectionStart) { + // First click - set start date + setSelectionStart(date); + setSelectionStartHour(null); // Reset hour selection + setInternalSelectedPeriod(null); + } else { + // Second click - complete selection + const start = new Date(Math.min(selectionStart.getTime(), date.getTime())); + const end = new Date(Math.max(selectionStart.getTime(), date.getTime())); + + // Check if any date in range is unavailable + let current = new Date(start); + let hasUnavailable = false; + while (current <= end) { + if (isDateUnavailable(current)) { + hasUnavailable = true; + break; + } + current.setDate(current.getDate() + 1); + } + + if (hasUnavailable) { + // Range contains unavailable dates, reset + setSelectionStart(null); + return; + } + + // Create rental selection + const rentalPeriod: UnavailablePeriod = { + id: 'rental-selection', + startDate: start, + endDate: end, + isRentalSelection: true + }; + + setInternalSelectedPeriod(rentalPeriod); + setSelectionStart(null); + setSelectionStartHour(null); + + // Notify parent if callback provided + if (onRentalSelect) { + onRentalSelect({ + startDate: start, + endDate: end + }); + } + } + } + }; + + + const toggleHourAvailability = (date: Date, hour: number) => { + if (mode !== 'owner') return; // Only owners can toggle hour availability + + // Check if this hour has an accepted rental + const hasAcceptedRental = unavailablePeriods.some(p => + p.isAcceptedRental && isHourUnavailable(date, hour) + ); + + if (hasAcceptedRental) { + return; // Can't modify hours with accepted rentals + } + + const isUnavailable = isHourUnavailable(date, hour); + + if (isUnavailable && !ownerSelectionStartHour) { + // Make just this hour available by splitting periods + const updatedPeriods: UnavailablePeriod[] = []; + + unavailablePeriods.forEach(period => { + if (period.isAcceptedRental || !isDateInPeriod(date, period)) { + // Keep accepted rentals and periods that don't contain this date + updatedPeriods.push(period); + } else if (!period.startTime || !period.endTime) { + // This is a full-day period, split it into hourly periods excluding this hour + for (let h = 0; h < 24; h++) { + if (h !== hour) { + updatedPeriods.push({ + id: `${period.id}-hour-${h}`, + startDate: date, + endDate: date, + startTime: `${h.toString().padStart(2, '0')}:00`, + endTime: `${h.toString().padStart(2, '0')}:59` + }); + } + } + } else { + // This is an hourly period, check if it contains this hour + const startHour = parseInt(period.startTime.split(':')[0]); + const endHour = parseInt(period.endTime.split(':')[0]); + + if (hour < startHour || hour > endHour) { + // Hour is outside this period, keep the period + updatedPeriods.push(period); + } else { + // Split the period around this hour + if (startHour < hour) { + updatedPeriods.push({ + id: period.id + '-before', + startDate: period.startDate, + endDate: period.endDate, + startTime: period.startTime, + endTime: `${(hour - 1).toString().padStart(2, '0')}:59` + }); + } + + if (endHour > hour) { + updatedPeriods.push({ + id: period.id + '-after', + startDate: period.startDate, + endDate: period.endDate, + startTime: `${(hour + 1).toString().padStart(2, '0')}:00`, + endTime: period.endTime + }); + } + } + } + }); + + onPeriodsChange(updatedPeriods); + setOwnerSelectionStart(null); + setOwnerSelectionStartHour(null); + } else if (!ownerSelectionStartHour) { + // First click - start selection + setOwnerSelectionStart(date); + setOwnerSelectionStartHour(hour); + } else { + // Second click - complete selection + const startDate = new Date(ownerSelectionStart!); + const endDate = new Date(date); + + // Determine which date/hour comes first + let finalStart, finalEnd, startHour, endHour; + + if (startDate.toDateString() === endDate.toDateString()) { + // Same day - compare hours + if (ownerSelectionStartHour <= hour) { + finalStart = startDate; + finalEnd = endDate; + startHour = ownerSelectionStartHour; + endHour = hour; + } else { + finalStart = endDate; + finalEnd = startDate; + startHour = hour; + endHour = ownerSelectionStartHour; + } + } else if (startDate < endDate) { + finalStart = startDate; + finalEnd = endDate; + startHour = ownerSelectionStartHour; + endHour = hour; + } else { + finalStart = endDate; + finalEnd = startDate; + startHour = hour; + endHour = ownerSelectionStartHour; + } + + // Check if any hour in range has accepted rental + let current = new Date(finalStart); + current.setHours(startHour, 0, 0, 0); + const endDateTime = new Date(finalEnd); + endDateTime.setHours(endHour, 59, 59, 999); + let hasAcceptedInRange = false; + + while (current <= endDateTime) { + const currentHour = current.getHours(); + if (unavailablePeriods.some(p => + p.isAcceptedRental && isHourUnavailable(current, currentHour) + )) { + hasAcceptedInRange = true; + break; + } + current.setHours(current.getHours() + 1); + } + + if (hasAcceptedInRange) { + // Can't select range with accepted rentals + setOwnerSelectionStart(null); + setOwnerSelectionStartHour(null); return; } - if (existingRentalPeriod) { - // Remove the rental selection - onPeriodsChange(unavailablePeriods.filter(p => p.id !== existingRentalPeriod.id)); - } else { - // Add new rental selection - const newPeriod: UnavailablePeriod = { - id: Date.now().toString(), - startDate: date, - endDate: date, - isRentalSelection: true - }; - onPeriodsChange([...unavailablePeriods, newPeriod]); - } - } else { - // Original behavior for marking unavailable - const existingPeriod = unavailablePeriods.find(period => { - const periodStart = new Date(period.startDate).toISOString().split('T')[0]; - const periodEnd = new Date(period.endDate).toISOString().split('T')[0]; - return periodStart === dateStr && periodEnd === dateStr && !period.startTime && !period.endTime; - }); - - if (existingPeriod) { - // Remove the period to make it available - onPeriodsChange(unavailablePeriods.filter(p => p.id !== existingPeriod.id)); - } else { - // Add new unavailable period for this date - const newPeriod: UnavailablePeriod = { - id: Date.now().toString(), - startDate: date, - endDate: date - }; - onPeriodsChange([...unavailablePeriods, newPeriod]); - } - } - }; - - const toggleHourAvailability = (date: Date, hour: number) => { - const startTime = `${hour.toString().padStart(2, '0')}:00`; - const endTime = `${hour.toString().padStart(2, '0')}:59`; - - const existingPeriod = unavailablePeriods.find(period => { - const periodDate = new Date(period.startDate).toISOString().split('T')[0]; - const checkDate = date.toISOString().split('T')[0]; - return periodDate === checkDate && - period.startTime === startTime && - period.endTime === endTime; - }); - - if (existingPeriod) { - // Remove the period to make it available - onPeriodsChange(unavailablePeriods.filter(p => p.id !== existingPeriod.id)); - } else { - // Add new unavailable period for this hour + // Create unavailable period const newPeriod: UnavailablePeriod = { id: Date.now().toString(), - startDate: date, - endDate: date, - startTime: startTime, - endTime: endTime + startDate: finalStart, + endDate: finalEnd, + startTime: `${startHour.toString().padStart(2, '0')}:00`, + endTime: `${endHour.toString().padStart(2, '0')}:59` }; + onPeriodsChange([...unavailablePeriods, newPeriod]); + setOwnerSelectionStart(null); + setOwnerSelectionStartHour(null); } }; @@ -341,45 +590,66 @@ const AvailabilityCalendar: React.FC = ({ if (date) { className += ' border cursor-pointer'; - const rentalPeriod = unavailablePeriods.find(p => - p.isRentalSelection && isDateInPeriod(date, p) - ); - - const acceptedRental = unavailablePeriods.find(p => - p.isAcceptedRental && isDateInPeriod(date, p) - ); - - // Check if date is the selection start - const isSelectionStart = selectionStart && date.getTime() === selectionStart.getTime(); - - // Check if date would be in the range if this was the end date - const wouldBeInRange = selectionStart && date >= - new Date(Math.min(selectionStart.getTime(), date.getTime())) && - date <= new Date(Math.max(selectionStart.getTime(), date.getTime())); - - if (acceptedRental) { - className += ' text-white'; - title = 'Booked - This date has an accepted rental'; - backgroundColor = '#6f42c1'; - } else if (rentalPeriod) { - className += ' bg-success text-white'; - title = isRentalMode ? 'Selected for rental - Click to clear' : 'Selected'; - } else if (isSelectionStart) { - className += ' bg-primary text-white'; - title = 'Start date selected - Click another date to complete selection'; - } else if (wouldBeInRange && !isDateFullyUnavailable(date) && !isDatePartiallyUnavailable(date)) { - className += ' bg-info bg-opacity-25'; - title = 'Click to set as end date'; - } else if (isDateFullyUnavailable(date)) { - className += ' bg-danger text-white'; - title = isRentalMode ? 'Unavailable' : 'Fully unavailable - Click to make available'; - } else if (isDatePartiallyUnavailable(date)) { - className += ' text-dark'; - title = isRentalMode ? 'Partially unavailable' : 'Partially unavailable - Click to view details'; - backgroundColor = '#ffeb3b'; + if (mode === 'owner') { + // OWNER VIEW + const acceptedRental = unavailablePeriods.find(p => + p.isAcceptedRental && isDateInPeriod(date, p) + ); + const isOwnerStart = ownerSelectionStart && date.getTime() === ownerSelectionStart.getTime(); + + if (acceptedRental) { + className += ' text-white'; + title = 'Booked - This date has an accepted rental'; + backgroundColor = '#6f42c1'; + } else if (isOwnerStart) { + className += ' bg-warning text-dark'; + title = 'Click another date to mark range as unavailable'; + } else if (isDateFullyUnavailable(date)) { + className += ' bg-danger text-white'; + title = 'Unavailable - Click to make available'; + } else if (isDatePartiallyUnavailable(date)) { + className += ' text-dark'; + title = 'Partially unavailable - Switch to week/day view for details'; + backgroundColor = '#ffeb3b'; + } else { + className += ' bg-light'; + title = ownerSelectionStart ? 'Click to complete selection' : 'Click to start selecting unavailable dates'; + } } else { - className += ' bg-light'; - title = isRentalMode ? 'Available - Click to select' : 'Available - Click to make unavailable'; + // RENTER VIEW + let isInSelectedPeriod = false; + if (selectedRentalPeriod) { + // Special handling for rental selections with specific end times + if (selectedRentalPeriod.endTime === '00:00') { + // If end time is midnight, don't include the end date in highlighting + const start = new Date(selectedRentalPeriod.startDate); + const end = new Date(selectedRentalPeriod.endDate); + const checkDate = new Date(date); + + start.setHours(0, 0, 0, 0); + end.setHours(0, 0, 0, 0); + checkDate.setHours(0, 0, 0, 0); + + // Exclude the end date if end time is 00:00 + isInSelectedPeriod = checkDate >= start && checkDate < end; + } else { + isInSelectedPeriod = isDateInPeriod(date, selectedRentalPeriod); + } + } + const isSelectionStart = selectionStart && date.getTime() === selectionStart.getTime(); + + if (isDateFullyUnavailable(date)) { + className += ' bg-danger text-white'; + } else if (isDatePartiallyUnavailable(date)) { + className += ' text-dark'; + backgroundColor = '#ffeb3b'; + } else if (isInSelectedPeriod) { + className += ' bg-success text-white'; + } else if (isSelectionStart) { + className += ' bg-success bg-opacity-50 text-dark'; + } else { + className += ' bg-light'; + } } } @@ -393,7 +663,7 @@ const AvailabilityCalendar: React.FC = ({ cursor: date ? 'pointer' : 'default', backgroundColor: backgroundColor }} - title={date ? title : ''} + title={mode === 'owner' && date ? title : ''} > {date?.getDate()} @@ -428,22 +698,93 @@ const AvailabilityCalendar: React.FC = ({ {hours.map(hour => ( - - {hour.toString().padStart(2, '0')}:00 + + {hour === 0 ? '12 AM' : hour === 12 ? '12 PM' : hour < 12 ? `${hour} AM` : `${hour - 12} PM`} {days.map((date, dayIndex) => { const isUnavailable = isHourUnavailable(date, hour); + const isClickable = mode === 'owner'; + const isOwnerSelectionStart = mode === 'owner' && ownerSelectionStart && + date.toDateString() === ownerSelectionStart.toDateString() && + hour === ownerSelectionStartHour; + + // Check if this hour is in the selected period + let isInSelectedPeriod = false; + if (mode === 'renter' && selectedRentalPeriod) { + const cellDate = new Date(date); + cellDate.setHours(0, 0, 0, 0); + const selStartDate = new Date(selectedRentalPeriod.startDate); + selStartDate.setHours(0, 0, 0, 0); + const selEndDate = new Date(selectedRentalPeriod.endDate); + selEndDate.setHours(0, 0, 0, 0); + + // Check if date is in range + if (cellDate >= selStartDate && cellDate <= selEndDate) { + // If times are specified, check hour range + if (selectedRentalPeriod.startTime && selectedRentalPeriod.endTime) { + const [startHour] = selectedRentalPeriod.startTime.split(':').map(Number); + const [endHour] = selectedRentalPeriod.endTime.split(':').map(Number); + + if (cellDate.getTime() === selStartDate.getTime() && cellDate.getTime() === selEndDate.getTime()) { + // Same day - check if hour is in range (end hour is exclusive) + isInSelectedPeriod = hour >= startHour && hour < endHour; + } else if (cellDate.getTime() === selStartDate.getTime()) { + // Start day - check if hour is after or at start hour + isInSelectedPeriod = hour >= startHour; + } else if (cellDate.getTime() === selEndDate.getTime()) { + // End day - check if hour is before end hour (exclusive) + isInSelectedPeriod = hour < endHour; + } else { + // Middle day - all hours selected + isInSelectedPeriod = true; + } + } else { + isInSelectedPeriod = true; + } + } + } + + const isSelectionStart = mode === 'renter' && selectionStart && + date.toDateString() === selectionStart.toDateString() && + hour === selectionStartHour; + + let cellClass = 'text-center p-1'; + if (isUnavailable) { + cellClass += ' bg-danger text-white'; + } else if (isInSelectedPeriod) { + cellClass += ' bg-success text-white'; + } else if (isOwnerSelectionStart) { + cellClass += ' bg-warning text-dark'; + } else if (isSelectionStart) { + cellClass += ' bg-success bg-opacity-50 text-dark'; + } else { + cellClass += ' bg-light'; + } + cellClass += (isClickable || (mode === 'renter' && !isUnavailable)) ? ' cursor-pointer' : ''; return ( toggleHourAvailability(date, hour)} - style={{ cursor: 'pointer', height: '30px' }} - title={isUnavailable ? 'Click to make available' : 'Click to make unavailable'} + className={cellClass} + onClick={() => { + if (mode === 'owner') { + toggleHourAvailability(date, hour); + } else if (mode === 'renter' && !isUnavailable) { + handleHourClick(date, hour); + } + }} + style={{ + cursor: (isClickable || (mode === 'renter' && !isUnavailable)) ? 'pointer' : 'default', + height: '40px', + borderLeft: dayIndex === 0 ? '1px solid #dee2e6' : 'none' + }} + title={ + mode === 'owner' + ? (isUnavailable ? 'Click to make this hour available' : (ownerSelectionStartHour !== null ? 'Click to complete selection' : 'Click to start selecting unavailable hours')) + : '' + } > - {isUnavailable && '×'} + {isUnavailable && ×} ); })} @@ -473,18 +814,85 @@ const AvailabilityCalendar: React.FC = ({ {hours.map(hour => { const isUnavailable = isHourUnavailable(currentDate, hour); + const isClickable = mode === 'owner'; + const isOwnerSelectionStart = mode === 'owner' && ownerSelectionStart && + currentDate.toDateString() === ownerSelectionStart.toDateString() && + hour === ownerSelectionStartHour; + + // Check if this hour is in the selected period + let isInSelectedPeriod = false; + if (mode === 'renter' && selectedRentalPeriod) { + const cellDate = new Date(currentDate); + cellDate.setHours(0, 0, 0, 0); + const selStartDate = new Date(selectedRentalPeriod.startDate); + selStartDate.setHours(0, 0, 0, 0); + const selEndDate = new Date(selectedRentalPeriod.endDate); + selEndDate.setHours(0, 0, 0, 0); + + // Check if date is in range + if (cellDate >= selStartDate && cellDate <= selEndDate) { + // If times are specified, check hour range + if (selectedRentalPeriod.startTime && selectedRentalPeriod.endTime) { + const [startHour] = selectedRentalPeriod.startTime.split(':').map(Number); + const [endHour] = selectedRentalPeriod.endTime.split(':').map(Number); + + if (cellDate.getTime() === selStartDate.getTime() && cellDate.getTime() === selEndDate.getTime()) { + // Same day - check if hour is in range (end hour is exclusive) + isInSelectedPeriod = hour >= startHour && hour < endHour; + } else if (cellDate.getTime() === selStartDate.getTime()) { + // Start day - check if hour is after or at start hour + isInSelectedPeriod = hour >= startHour; + } else if (cellDate.getTime() === selEndDate.getTime()) { + // End day - check if hour is before end hour (exclusive) + isInSelectedPeriod = hour < endHour; + } else { + // Middle day - all hours selected + isInSelectedPeriod = true; + } + } else { + isInSelectedPeriod = true; + } + } + } + + const isSelectionStart = mode === 'renter' && selectionStart && + currentDate.toDateString() === selectionStart.toDateString() && + hour === selectionStartHour; + + let cellClass = 'text-center p-3'; + if (isUnavailable) { + cellClass += ' bg-danger text-white'; + } else if (isInSelectedPeriod) { + cellClass += ' bg-success text-white'; + } else if (isOwnerSelectionStart) { + cellClass += ' bg-warning text-dark'; + } else if (isSelectionStart) { + cellClass += ' bg-success bg-opacity-50 text-dark'; + } else { + cellClass += ' bg-light'; + } + cellClass += (isClickable || (mode === 'renter' && !isUnavailable)) ? ' cursor-pointer' : ''; return ( - {hour.toString().padStart(2, '0')}:00 + {hour === 0 ? '12:00 AM' : hour === 12 ? '12:00 PM' : hour < 12 ? `${hour}:00 AM` : `${hour - 12}:00 PM`} toggleHourAvailability(currentDate, hour)} - style={{ cursor: 'pointer' }} - title={isUnavailable ? 'Click to make available' : 'Click to make unavailable'} + className={cellClass} + onClick={() => { + if (mode === 'owner') { + toggleHourAvailability(currentDate, hour); + } else if (mode === 'renter' && !isUnavailable) { + handleHourClick(currentDate, hour); + } + }} + style={{ cursor: (isClickable || (mode === 'renter' && !isUnavailable)) ? 'pointer' : 'default' }} + title={ + mode === 'owner' + ? (isUnavailable ? 'Click to make this hour available' : (ownerSelectionStartHour !== null ? 'Click to complete selection' : 'Click to start selecting unavailable hours')) + : '' + } > {isUnavailable ? 'Unavailable' : 'Available'} @@ -527,24 +935,20 @@ const AvailabilityCalendar: React.FC = ({ > Month - {priceType === 'hour' && ( - <> - - - - )} + +
@@ -572,22 +976,22 @@ const AvailabilityCalendar: React.FC = ({
-
- {isRentalMode ? 'Click start date, then click end date to select rental period' : 'Click on any date or time slot to toggle availability'} -
- {viewType === 'month' && ( -
- Available - {isRentalMode && ( - Selected - )} - {!isRentalMode && ( - Booked - )} +
+ Available + {mode === 'renter' && selectedRentalPeriod && ( + Your Selection + )} + {mode === 'owner' && ownerSelectionStart && ( + Selection Start + )} + {mode === 'owner' && ( + Booked Rental + )} + {viewType === 'month' && ( Partially Unavailable - Fully Unavailable -
- )} + )} + {mode === 'owner' ? 'Marked Unavailable' : 'Not Available'} +
); diff --git a/frontend/src/components/PrivateRoute.tsx b/frontend/src/components/PrivateRoute.tsx index 4b1e841..e74aea4 100644 --- a/frontend/src/components/PrivateRoute.tsx +++ b/frontend/src/components/PrivateRoute.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { Navigate } from 'react-router-dom'; +import { Navigate, useLocation } from 'react-router-dom'; import { useAuth } from '../contexts/AuthContext'; interface PrivateRouteProps { @@ -8,6 +8,7 @@ interface PrivateRouteProps { const PrivateRoute: React.FC = ({ children }) => { const { user, loading } = useAuth(); + const location = useLocation(); if (loading) { return ( @@ -19,7 +20,7 @@ const PrivateRoute: React.FC = ({ children }) => { ); } - return user ? <>{children} : ; + return user ? <>{children} : ; }; export default PrivateRoute; \ No newline at end of file diff --git a/frontend/src/pages/CreateItem.tsx b/frontend/src/pages/CreateItem.tsx index affbd0e..94f1c6b 100644 --- a/frontend/src/pages/CreateItem.tsx +++ b/frontend/src/pages/CreateItem.tsx @@ -435,7 +435,7 @@ const CreateItem: React.FC = () => { onPeriodsChange={(periods) => setFormData(prev => ({ ...prev, unavailablePeriods: periods })) } - priceType={priceType} + mode="owner" /> diff --git a/frontend/src/pages/EditItem.tsx b/frontend/src/pages/EditItem.tsx index f09202e..e52ab29 100644 --- a/frontend/src/pages/EditItem.tsx +++ b/frontend/src/pages/EditItem.tsx @@ -546,7 +546,7 @@ const EditItem: React.FC = () => { const userPeriods = periods.filter(p => !p.isAcceptedRental); setFormData(prev => ({ ...prev, unavailablePeriods: userPeriods })); }} - priceType={priceType} + mode="owner" /> diff --git a/frontend/src/pages/ItemDetail.tsx b/frontend/src/pages/ItemDetail.tsx index eec160c..55ecb78 100644 --- a/frontend/src/pages/ItemDetail.tsx +++ b/frontend/src/pages/ItemDetail.tsx @@ -18,6 +18,9 @@ const ItemDetail: React.FC = () => { useEffect(() => { fetchItem(); + }, [id]); + + useEffect(() => { if (user) { checkIfAlreadyRenting(); } @@ -207,7 +210,7 @@ const ItemDetail: React.FC = () => { -
+
{isOwner ? (
-
Delivery Options
- -
- +