diff --git a/.gitignore b/.gitignore index 0841d7afc..f9b3032ee 100644 --- a/.gitignore +++ b/.gitignore @@ -74,6 +74,7 @@ server/.env # Secret certificates (should not be committed) server/config/*.crt +server/private/sessions # build build/ diff --git a/frontend/src/components/RequestRideModal/RequestRideInfo.tsx b/frontend/src/components/RequestRideModal/RequestRideInfo.tsx index adb127942..a52df574f 100644 --- a/frontend/src/components/RequestRideModal/RequestRideInfo.tsx +++ b/frontend/src/components/RequestRideModal/RequestRideInfo.tsx @@ -452,6 +452,7 @@ const RequestRideInfo: React.FC = ({ availableLocations={locations} onPickupSelect={handlePickupSelect} onDropoffSelect={handleDropoffSelect} + kmlLink={null} /> diff --git a/frontend/src/components/RiderComponents/RequestRideDialog.tsx b/frontend/src/components/RiderComponents/RequestRideDialog.tsx index 04f90fed6..c87092de7 100644 --- a/frontend/src/components/RiderComponents/RequestRideDialog.tsx +++ b/frontend/src/components/RiderComponents/RequestRideDialog.tsx @@ -18,6 +18,8 @@ import { InputLabel, SelectChangeEvent, FormHelperText, + Switch, + Alert, } from '@mui/material'; import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns'; import { @@ -25,13 +27,13 @@ import { DatePicker, TimePicker, } from '@mui/x-date-pickers'; + import { APIProvider } from '@vis.gl/react-google-maps'; import RequestRideMap from './RequestRideMap'; import styles from './requestridedialog.module.css'; import { Ride, Location, Tag } from 'types'; import RequestRidePlacesSearch from './RequestRidePlacesSearch'; import axios from '../../util/axios'; -import { error } from 'console'; type RepeatOption = 'none' | 'daily' | 'weekly' | 'custom'; @@ -125,11 +127,14 @@ const RequestRideDialog: React.FC = ({ useState('pickup'); const [pendingLocation, setPendingLocation] = useState(null); const [confirmDialogOpen, setConfirmDialogOpen] = useState(false); - const [customPickup, setCustomPickup] = useState(false); - const [customDroppoff, setCustomDropoff] = useState(false); const [inputPickUpError, setInputPickUpError] = useState(false); const [inputDropOffError, setInputDropOffError] = useState(false); const [inputErrorText, setInputErrorText] = useState(''); + const [customPickup, setCustomPickup] = useState(false); + const [customDropoff, setCustomDropoff] = useState(false); + const [overlayRadius, setOverlayRadius] = useState(false); + const [kmlLink, setKMLLink] = useState(null); + const [isInvalidTime, setIsInvalidTime] = useState(false); useEffect(() => { if (ride && open) { @@ -173,6 +178,15 @@ const RequestRideDialog: React.FC = ({ } }; + /** + * Finalizes pending location selection after user confirms + * + * Depending on the current 'selectionState': + * - Saves as the pickup location or + * - Saves as the dropoff location + * + * Pprogresses the state machine to the next step. + */ /** * Finalizes pending location selection after user confirms * @@ -202,6 +216,12 @@ const RequestRideDialog: React.FC = ({ setConfirmDialogOpen(false); }; + /** + * Resets the pickup/dropoff flow back to the beginning. + * + * Clears both selected locations and returns to the "pickup" phase. + */ + /** * Resets the pickup/dropoff flow back to the beginning. * @@ -237,6 +257,7 @@ const RequestRideDialog: React.FC = ({ if (selectionState === 'pickup') { // Show all locations for pickup selection return supportLocsWithOther; + return supportLocsWithOther; } else if (selectionState === 'dropoff') { // Show all locations except the selected pickup location return supportLocsWithOther.filter( @@ -431,12 +452,31 @@ const RequestRideDialog: React.FC = ({ const handleDateChange = (field: keyof Pick) => (newDate: Date | null) => { + if (field === 'time') { + const isValid = + newDate !== null + ? isValidTime(newDate) + : setFormData({ + ...formData, + [field]: newDate, + }); + setIsInvalidTime(!isValid); + } + setFormData({ ...formData, [field]: newDate, }); }; + const isValidTime = (time: Date) => { + if (!time) return true; + const input_minutes = time.getHours() * 60 + time.getMinutes(); //converts input time to min + const start = 7 * 60 + 45; // 7:45 AM in min + const end = 22 * 60; // 10:00 PM in min + return input_minutes >= start && input_minutes <= end; + }; + const handleRepeatTypeChange = ( event: React.ChangeEvent ) => { @@ -526,7 +566,8 @@ const RequestRideDialog: React.FC = ({ formData.time && (formData.repeatType !== 'custom' || formData.selectedDays.length > 0) && (formData.repeatType === 'none' || formData.repeatEndDate) && - selectionState === 'complete' + selectionState === 'complete' && + isInvalidTime === false ); }; @@ -538,6 +579,21 @@ const RequestRideDialog: React.FC = ({ ); }; + useEffect(() => { + if (overlayRadius) { + setKMLLink( + 'https://www.google.com/maps/d/u/1/kml?forcekml=1&mid=1jE3AC5mAzZK0i29WGiOQiLkkb0ViXrU&lid=0ZWUOMlDE24' + ); + } else { + setKMLLink(null); + } + }, [overlayRadius]); + + const isWeekend = (date: Date) => { + const day = date.getDay(); + return day === 0 || day === 6; + }; + // Ensures the map only receives valid coordinates; prevents rendering issues const safePickup = formData.pickupLocation?.lat && formData.pickupLocation?.lng @@ -553,6 +609,7 @@ const RequestRideDialog: React.FC = ({ return ( {!ride ? 'Request a Ride' : 'Edit Ride'} + {/* Wrap everything in a single APIProvider */} = ({ {/* Dropdown selectors as alternative to map selection */}
-

- Or select from dropdown: -

- - - Pickup Location - - value={formData.pickupLocation?.id || ''} - onChange={(event) => { - const locationId = event.target.value as string; - const selectedLocation = - supportLocsWithOther.find( - (loc) => loc.id === locationId - ) || null; - setFormData((prev) => ({ - ...prev, - pickupLocation: selectedLocation, - })); - // Update selection state - if (selectedLocation && !formData.dropoffLocation) { - setSelectionState('dropoff'); - } else if ( - selectedLocation && - formData.dropoffLocation - ) { - setSelectionState('complete'); - } - if (selectedLocation?.name === Other.name) { - setCustomPickup(true); - } else { - setCustomPickup(false); - } - //remove error if user changes location - setInputPickUpError(false); - }} - label="Pickup Location" - error={inputPickUpError} - > - {supportLocsWithOther.map((location) => ( - - {location.name} - - ))} - - {inputPickUpError && ( - {inputErrorText} - )} - - - {customPickup === true && ( -
- -
- )} - - - Drop-off Location - - value={formData.dropoffLocation?.id || ''} - disabled={!formData.pickupLocation} //makes ensuring start and end locations are different simpler - onChange={(event) => { - const locationId = event.target.value as string; - const selectedLocation = - supportLocsWithOther.find( - (loc) => loc.id === locationId - ) || null; - setFormData((prev) => ({ - ...prev, - dropoffLocation: selectedLocation, - })); - // Update selection state - if (selectedLocation && formData.pickupLocation) { - setSelectionState('complete'); - } - if (selectedLocation?.name === Other.name) { - setCustomDropoff(true); - } else { - setCustomDropoff(false); - } - //remove error if user changes locatoin - setInputDropOffError(false); - }} - label="Drop-off Location" - error={inputDropOffError} - > - {supportLocsWithOther - .filter( - (loc) => - loc.id.startsWith('custom') || - loc.id !== formData.pickupLocation?.id - ) // Don't show pickup location as dropoff option (except Other) - .map((location) => ( +
+

+ Or select from dropdown: +

+ + + Pickup Location + + value={formData.pickupLocation?.id || ''} + onChange={(event) => { + const locationId = event.target.value as string; + const selectedLocation = + supportLocsWithOther.find( + (loc) => loc.id === locationId + ) || null; + setFormData((prev) => ({ + ...prev, + pickupLocation: selectedLocation, + })); + // Update selection state + if (selectedLocation && !formData.dropoffLocation) { + setSelectionState('dropoff'); + } else if ( + selectedLocation && + formData.dropoffLocation + ) { + setSelectionState('complete'); + } + if (selectedLocation?.name === Other.name) { + setCustomPickup(true); + } else { + setCustomPickup(false); + } + //remove error if user changes location + setInputPickUpError(false); + }} + label="Pickup Location" + error={inputPickUpError} + > + {supportLocsWithOther.map((location) => ( {location.name} ))} - - {inputDropOffError && ( - {inputErrorText} - )} - {!formData.pickupLocation && ( - - {'Please choose pickup location first'} - + + {inputPickUpError && ( + {inputErrorText} + )} + + + {customPickup === true && ( +
+ +
)} - - {customDroppoff === true && ( -
- -
- )} -
+ + Drop-off Location + + value={formData.dropoffLocation?.id || ''} + disabled={!formData.pickupLocation} //makes ensuring start and end locations are different simpler + onChange={(event) => { + const locationId = event.target.value as string; + const selectedLocation = + supportLocsWithOther.find( + (loc) => loc.id === locationId + ) || null; + setFormData((prev) => ({ + ...prev, + dropoffLocation: selectedLocation, + })); + // Update selection state + if (selectedLocation && formData.pickupLocation) { + setSelectionState('complete'); + } + if (selectedLocation?.name === Other.name) { + setCustomDropoff(true); + } else { + setCustomDropoff(false); + } + //remove error if user changes locatoin + setInputDropOffError(false); + }} + label="Drop-off Location" + error={inputDropOffError} + > + {supportLocsWithOther + .filter( + (loc) => + loc.id.startsWith('custom') || + loc.id !== formData.pickupLocation?.id + ) // Don't show pickup location as dropoff option (except Other) + .map((location) => ( + + {location.name} + + ))} + + {inputDropOffError && ( + {inputErrorText} + )} + {!formData.pickupLocation && ( + + {'Please choose pickup location first'} + + )} + + + {customDropoff === true && ( +
+ +
+ )} - - - - - - - - - Repeat Options - - {repeatOptions.map((option) => ( - } - label={option.label} - /> - ))} - - + {customDropoff === true && ( +
+ +
+ )} +
- {formData.repeatType !== 'none' && ( - + + + + + {isInvalidTime && ( + + Invalid time! CU Lift operates between 7:45 AM and 10 + PM. + + )} - )} - {formData.repeatType === 'custom' && ( -
- - Select Days - - + Repeat Options + - {daysOfWeek.map((day) => ( - - {day} - + {repeatOptions.map((option) => ( + } + label={option.label} + /> ))} - -
- )} + + + + {formData.repeatType !== 'none' && ( + + + + )} + + {formData.repeatType === 'custom' && ( +
+ + Select Days + + + {daysOfWeek.map((day) => ( + + {day} + + ))} + +
+ )} + -
+
+ setOverlayRadius(e.target.checked)} + /> + } + label="CULift Service Radius" + /> +
+
diff --git a/frontend/src/components/RiderComponents/RequestRideMap.tsx b/frontend/src/components/RiderComponents/RequestRideMap.tsx index c29225665..84c500d0b 100644 --- a/frontend/src/components/RiderComponents/RequestRideMap.tsx +++ b/frontend/src/components/RiderComponents/RequestRideMap.tsx @@ -27,6 +27,7 @@ interface RequestRideMapProps { availableLocations?: Location[]; onPickupSelect: (location: Location | null) => void; onDropoffSelect: (location: Location | null) => void; + kmlLink: string | null; } const RequestRideMap: React.FC = ({ @@ -36,6 +37,7 @@ const RequestRideMap: React.FC = ({ availableLocations = [], onPickupSelect, onDropoffSelect, + kmlLink, }) => { const map = useMap(); const polylineRef = useRef(null); @@ -59,6 +61,23 @@ const RequestRideMap: React.FC = ({ }; }, [map]); + //adds CU-Lift radius to map + useEffect(() => { + if (!map || !kmlLink) return; + + const kmlLayer = new google.maps.KmlLayer({ + url: kmlLink, + map: map, + preserveViewport: true, + suppressInfoWindows: true, + }); + + return () => { + kmlLayer.setMap(null); + }; +}, [map, kmlLink]); + + const handleMarkerRef = useCallback((marker: Marker | null, id: string) => { if (marker) markers.current[id] = marker; else delete markers.current[id]; @@ -164,6 +183,7 @@ const RequestRideMap: React.FC = ({ gestureHandling={'greedy'} disableDefaultUI={false} className={styles.mapContainer} + > {/* Pickup Location Marker */} {pickupLocation && ( diff --git a/frontend/src/components/RiderComponents/requestridedialog.module.css b/frontend/src/components/RiderComponents/requestridedialog.module.css index 7b01b0996..40d60d870 100644 --- a/frontend/src/components/RiderComponents/requestridedialog.module.css +++ b/frontend/src/components/RiderComponents/requestridedialog.module.css @@ -16,6 +16,8 @@ height: 100%; min-width: 0; position: relative; + display: flex; + flex-direction: column; } .formSection { @@ -30,14 +32,10 @@ /* Map container */ .mapContainer { width: 100%; - height: 100%; + flex: 1; /* Take remaining space */ border-radius: 4px; overflow: hidden; - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; + position: relative; /* Change from absolute */ background-color: #f5f5f5; }