Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions src/app/_components/agencycomponents/agency-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,11 @@ interface AgencyBookingForm {

interface AgencyFormProps {
form: UseFormReturnType<AgencyBookingForm>;
pickupAddressRef: React.RefObject<HTMLInputElement | null>;
destinationAddressRef: React.RefObject<HTMLInputElement | null>;
}

export const AgencyForm = ({ form, destinationAddressRef }: AgencyFormProps) => {
export const AgencyForm = ({ form, pickupAddressRef, destinationAddressRef }: AgencyFormProps) => {
const now = new Date();

return (
Expand Down Expand Up @@ -150,7 +151,9 @@ export const AgencyForm = ({ form, destinationAddressRef }: AgencyFormProps) =>
label="Pickup Address"
placeholder="Enter address"
key={form.key("pickupAddress")}
{...form.getInputProps("pickupAddress")}
onChange={form.getInputProps("pickupAddress").onChange}
error={form.getInputProps("pickupAddress").error}
ref={pickupAddressRef}
/>
</div>
<div className={classes.formRow}>
Expand All @@ -159,7 +162,8 @@ export const AgencyForm = ({ form, destinationAddressRef }: AgencyFormProps) =>
label="Destination Address"
placeholder="Enter address"
key={form.key("destinationAddress")}
{...form.getInputProps("destinationAddress")}
onChange={form.getInputProps("destinationAddress").onChange}
error={form.getInputProps("destinationAddress").error}
ref={destinationAddressRef}
/>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export const BookingInteractiveArea = ({
const [currentDate, setCurrentDate] = useState<Date>(new Date());
const [isDayView, setIsDayView] = useState<boolean>(false);
const [validationAddressGood, setValidationAddressGood] = useState<boolean>(false);
const [validationPickupAddressGood, setValidationPickupAddressGood] = useState<boolean>(false);

const utils = api.useUtils();
const {
Expand Down Expand Up @@ -79,6 +80,7 @@ export const BookingInteractiveArea = ({
//Starts off as null but will equal (through inputElement.current) an HTML input element when assigned at runtime
//Will be assigned an HTML input element when the mantine form loads
const inputElement = useRef<HTMLInputElement | null>(null);
const pickupInputElement = useRef<HTMLInputElement | null>(null);

//Load the Google maps API
const { isLoaded } = useLoadScript({
Expand Down Expand Up @@ -128,6 +130,10 @@ export const BookingInteractiveArea = ({
},
});

//Keep a stable ref to form.setFieldValue so the autocomplete useEffects don't re-run when the form re-renders
const setFieldValueRef = useRef(form.setFieldValue);
setFieldValueRef.current = form.setFieldValue;

useEffect(() => {
if (selectedBooking && currentModalMode === "edit") {
let residentName = "";
Expand Down Expand Up @@ -157,7 +163,13 @@ export const BookingInteractiveArea = ({
destinationAddress: selectedBooking.destinationAddress,
});

// Sync form state to the uncontrolled address inputs directly via their refs
if (inputElement.current) inputElement.current.value = selectedBooking.destinationAddress;
if (pickupInputElement.current)
pickupInputElement.current.value = selectedBooking.pickupAddress;

setValidationAddressGood(true);
setValidationPickupAddressGood(true);
}
}, [selectedBooking, currentModalMode, form.setValues]);

Expand Down Expand Up @@ -189,6 +201,8 @@ export const BookingInteractiveArea = ({
if (value.geometry) {
//The value is one of google's suggested locations. Geometry field is set if picked location is valid
setValidationAddressGood(true);
//Sync the Google-selected address back into Mantine form state
setFieldValueRef.current("destinationAddress", inputElement.current?.value ?? "");
} else {
//Google maps api could not figure out what the user gave it
setValidationAddressGood(false);
Expand All @@ -208,6 +222,58 @@ export const BookingInteractiveArea = ({
};
}, [inputElement.current]);

//Run the following every time pickupInputElement.current gets a new value (occurs whenever pickupAddress field changes in mantine form)
// biome-ignore lint: pickupInputElement.current does change and needs to be changed in order for useEffect() to run
useEffect(() => {
//Do not run the code within the useEffect if the api or mantine form hasn't fully loaded
if (!isLoaded || google.maps.places === null || pickupInputElement.current === null) {
return;
}

//Make the google auto complete element and attach it to pickupInputElement (requires an HTML input element to mount to)
const googleAutoCompleteElement = new google.maps.places.Autocomplete(
pickupInputElement.current,
{
types: ["address"],
},
);

//Places bounds on what locations google will suggest
googleAutoCompleteElement.setComponentRestrictions({
country: ["ca"], //Only show places in Canada
});

//Specifies which values we want to grab when the user makes an API call
googleAutoCompleteElement.setFields(["geometry"]);

//Attaches an event listener whenever the element's field changes
googleAutoCompleteElement.addListener("place_changed", () => {
const value = googleAutoCompleteElement.getPlace(); //Gets the value the user inputted

if (value.geometry) {
//The value is one of google's suggested locations. Geometry field is set if picked location is valid
setValidationPickupAddressGood(true);
//Sync the Google-selected address back into Mantine form state
setFieldValueRef.current("pickupAddress", pickupInputElement.current?.value ?? "");
} else {
//Google maps api could not figure out what the user gave it
setValidationPickupAddressGood(false);
}
});

//Adds an event to pickupInputElement.current (now that it's not null)
function pickupInputElementOnInput() {
setValidationPickupAddressGood(false); //User started typing, assume input is invalid
}
pickupInputElement.current.addEventListener("input", pickupInputElementOnInput);

//When pickupInputElement.current changes, remove the listeners from old elements to prevent any performance issues
return () => {
google.maps.event.clearInstanceListeners(googleAutoCompleteElement); //Removes the place_changed event listener to the old element
pickupInputElement.current?.removeEventListener("input", pickupInputElementOnInput); //Removes the listener defined as pickupInputElementOnInput from pickupInputElement.current before replacing it
};
}, [pickupInputElement.current]);
Comment thread
promatty marked this conversation as resolved.

const handleConfirm = () => {
const validation = form.validate();
const hasErrors = Object.keys(validation.errors).length > 0;
Expand All @@ -217,6 +283,11 @@ export const BookingInteractiveArea = ({
return;
}

if (!validationPickupAddressGood) {
form.setFieldError("pickupAddress", "Please select a valid address from the dropdown");
return;
}

if (!validationAddressGood) {
form.setFieldError("destinationAddress", "Please select a valid address from the dropdown");
return;
Expand All @@ -242,7 +313,7 @@ export const BookingInteractiveArea = ({
phoneNumber: form.values.phoneNumber,
additionalInfo: form.values.additionalInfo,
pickupAddress: form.values.pickupAddress,
destinationAddress: inputElement.current?.value || "",
destinationAddress: form.values.destinationAddress,
startTime: form.values.startTime,
endTime: form.values.endTime,
purpose: form.values.purpose,
Expand All @@ -254,7 +325,7 @@ export const BookingInteractiveArea = ({
id: selectedBooking.id,
title: form.values.title,
pickupAddress: form.values.pickupAddress,
destinationAddress: inputElement.current?.value || "",
destinationAddress: form.values.destinationAddress,
purpose: form.values.purpose,
passengerInfo,
phoneNumber: form.values.phoneNumber,
Expand All @@ -266,8 +337,12 @@ export const BookingInteractiveArea = ({

const handleModalCleanup = () => {
form.reset();
// Clear the uncontrolled address inputs directly via their refs
if (inputElement.current) inputElement.current.value = "";
if (pickupInputElement.current) pickupInputElement.current.value = "";
setShowBookingModal(false);
setValidationAddressGood(false);
setValidationPickupAddressGood(false);
setSelectedBooking(undefined);
};

Expand All @@ -276,6 +351,7 @@ export const BookingInteractiveArea = ({
setSelectedBooking(undefined);
form.reset();
setValidationAddressGood(false);
setValidationPickupAddressGood(false);
setShowBookingModal(true);
};

Expand Down Expand Up @@ -346,7 +422,11 @@ export const BookingInteractiveArea = ({
confirmText={currentModalMode === "create" ? "Confirm Booking" : "Save Changes"}
loading={createBookingMutation.isPending || updateBookingMutation.isPending}
>
<AgencyForm form={form} destinationAddressRef={inputElement} />
<AgencyForm
form={form}
pickupAddressRef={pickupInputElement}
destinationAddressRef={inputElement}
/>
</Modal>
</>
);
Expand Down
Loading