diff --git a/client/src/components/GoogleMapsScriptLoader.tsx b/client/src/components/GoogleMapsScriptLoader.tsx new file mode 100644 index 0000000..2f8d26c --- /dev/null +++ b/client/src/components/GoogleMapsScriptLoader.tsx @@ -0,0 +1,35 @@ +import { useEffect } from "react"; + +const GOOGLE_API_KEY = import.meta.env.VITE_GOOGLE_API_KEY; + +interface GoogleMapsScriptLoaderProps { + onLoad: () => void; +} + +const GoogleMapsScriptLoader: React.FC = ({ onLoad }) => { + useEffect(() => { + if (window.google) { + onLoad(); + return; + } + + if (document.getElementById("google-maps-script")) { + // If script already exists but not yet loaded, add onload + const script = document.getElementById("google-maps-script") as HTMLScriptElement; + script.onload = onLoad; + return; + } + + const script = document.createElement("script"); + script.id = "google-maps-script"; + script.src = `https://maps.googleapis.com/maps/api/js?key=${GOOGLE_API_KEY}&libraries=places`; + script.async = true; + script.defer = true; + script.onload = onLoad; + document.head.appendChild(script); + }, [onLoad]); + + return null; +}; + +export default GoogleMapsScriptLoader; \ No newline at end of file diff --git a/client/src/components/TripDashboard.tsx b/client/src/components/TripDashboard.tsx index bdd895d..1d5186e 100644 --- a/client/src/components/TripDashboard.tsx +++ b/client/src/components/TripDashboard.tsx @@ -60,6 +60,9 @@ const TripDashboard: React.FC = () => { selectedVehicle === "All" ? true : trip.vehicle?.name === selectedVehicle ); + //Google directions Start + + //Google directions End return ( { ))} -
+
+ {/* Start of direction button */} + + {/* End of direction button */}
); diff --git a/client/src/components/TripForm.tsx b/client/src/components/TripForm.tsx index 9e8e48c..08a1693 100644 --- a/client/src/components/TripForm.tsx +++ b/client/src/components/TripForm.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useState, useRef, useEffect } from "react"; import { gql, useMutation, useQuery } from "@apollo/client"; import { GET_VEHICLES } from "../graphql/vehicleQueries"; import { GET_ALERT_MESSAGES } from "../graphql/maintenanceQueries"; @@ -10,8 +10,8 @@ import Card from "./Card"; import Button from "./Button"; import { baseFieldStyles, selectFieldStyles } from "../../styles/styles"; import confetti from "canvas-confetti"; +import GoogleMapsScriptLoader from "./GoogleMapsScriptLoader"; -// const GOOGLE_API_KEY = import.meta.env.VITE_GOOGLE_API_KEY; const ADD_TRIP = gql` mutation AddTrip( @@ -89,25 +89,6 @@ const US_STATES = [ "WY", ]; -// const loadGoogleMapsScript = (apiKey: string) => { -// if (!apiKey) { -// console.error("Google Maps API key is missing!"); -// return Promise.reject("Missing API key"); - -// } -// if (window.google) return Promise.resolve(); - -// return new Promise((resolve, reject) => { -// const script = document.createElement("script"); -// script.src = `https://maps.googleapis.com/maps/api/js?key=${apiKey}&libraries=places`; -// script.async = true; -// script.defer = true; -// script.onload = () => resolve(); -// script.onerror = () => reject("Failed to load Google Maps script."); -// document.head.appendChild(script); -// }); -// }; - const getTodayDate = (): string => { const today = new Date(); const offsetDate = new Date( @@ -117,6 +98,7 @@ const getTodayDate = (): string => { }; const TripForm: React.FC<{ onTripAdded?: () => void }> = ({ onTripAdded }) => { + const [googleMapsLoaded, setGoogleMapsLoaded] = useState(false); const [formState, setFormState] = useState({ startStreet: "", startCity: "", @@ -126,6 +108,8 @@ const TripForm: React.FC<{ onTripAdded?: () => void }> = ({ onTripAdded }) => { endState: "", vehicleId: "", departureDate: getTodayDate(), + startLocation: "", + endLocation: "", }); const [confirmation, setConfirmation] = useState<{ @@ -150,81 +134,106 @@ const TripForm: React.FC<{ onTripAdded?: () => void }> = ({ onTripAdded }) => { }); -// const startStreetRef = useRef(null); -// const endStreetRef = useRef(null); - -// useEffect(() => { -// const initAutocomplete = async () => { -// try { -// await loadGoogleMapsScript(GOOGLE_API_KEY); - -// if (window.google && startStreetRef.current) { -// const startAutocomplete = new google.maps.places.Autocomplete( -// startStreetRef.current, -// { types: ["address"], componentRestrictions: { country: "us" } } -// ); - -// startAutocomplete.addListener("place_changed", () => { -// const place = startAutocomplete.getPlace(); -// if (place.address_components) { -// const city = place.address_components.find((c) => -// c.types.includes("locality") -// )?.long_name; -// const state = place.address_components.find((c) => -// c.types.includes("administrative_area_level_1") -// )?.short_name; - -// setFormState((prev) => ({ -// ...prev, -// startStreet: place.formatted_address || prev.startStreet, -// startCity: city || prev.startCity, -// startState: state || prev.startState, -// })); -// } -// }); -// } - -// if (window.google && endStreetRef.current) { -// const endAutocomplete = new google.maps.places.Autocomplete( -// endStreetRef.current, -// { types: ["address"], componentRestrictions: { country: "us" } } -// ); - -// endAutocomplete.addListener("place_changed", () => { -// const place = endAutocomplete.getPlace(); -// if (place.address_components) { -// const city = place.address_components.find((c) => -// c.types.includes("locality") -// )?.long_name; -// const state = place.address_components.find((c) => -// c.types.includes("administrative_area_level_1") -// )?.short_name; - -// setFormState((prev) => ({ -// ...prev, -// endStreet: place.formatted_address || prev.endStreet, -// endCity: city || prev.endCity, -// endState: state || prev.endState, -// })); -// } -// }); -// } -// } catch (error) { -// console.error(error); -// } -// }; - -// initAutocomplete(); -// }, []); + const startStreetRef = useRef(null); + const endStreetRef = useRef(null); + + useEffect(() => { + if (!googleMapsLoaded) return; + + if (window.google && startStreetRef.current) { + const startAutocomplete = new google.maps.places.Autocomplete( + startStreetRef.current, + { types: ["address"], componentRestrictions: { country: "us" } } + ); + + startAutocomplete.addListener("place_changed", () => { + const place = startAutocomplete.getPlace(); + if (place.address_components) { + const streetNumber = place.address_components.find((c) => + c.types.includes("street_number") + )?.long_name || ""; + const route = place.address_components.find((c) => + c.types.includes("route") + )?.long_name || ""; + const street = `${streetNumber} ${route}`.trim(); + + const city = place.address_components.find((c) => c.types.includes("locality"))?.long_name; + const state = place.address_components.find((c) => + c.types.includes("administrative_area_level_1") + )?.short_name; + + setFormState((prev) => ({ + ...prev, + startStreet: street || prev.startStreet, + startCity: city || prev.startCity, + startState: state || prev.startState, + startLocation: place.formatted_address || prev.startLocation, + })); + } + }); +} + +if (window.google && endStreetRef.current) { + const endAutocomplete = new google.maps.places.Autocomplete( + endStreetRef.current, + { types: ["address"], componentRestrictions: { country: "us" } } + ); + + endAutocomplete.addListener("place_changed", () => { + const place = endAutocomplete.getPlace(); + if (place.address_components) { + const city = place.address_components.find((c) => c.types.includes("locality"))?.long_name; + const state = place.address_components.find((c) => + c.types.includes("administrative_area_level_1") + )?.short_name; + + // Extract just the street address for endStreet + const streetNumber = place.address_components.find((c) => + c.types.includes("street_number") + )?.long_name || ""; + const route = place.address_components.find((c) => + c.types.includes("route") + )?.long_name || ""; + + const street = [streetNumber, route].filter(Boolean).join(" "); + + setFormState((prev) => ({ + ...prev, + endStreet: street || prev.endStreet, + endCity: city || prev.endCity, + endState: state || prev.endState, + endLocation: place.formatted_address || prev.endLocation, + })); + } + }); +} + }, [googleMapsLoaded]); const handleChange = ( e: React.ChangeEvent ) => { const { name, value } = e.target; - setFormState((prev) => ({ - ...prev, - [name]: value, - })); + + if (name === "startStreet") { + setFormState((prev) => ({ + ...prev, + startStreet: value, + startCity: "", + startState: "", + })); + } else if (name === "endStreet") { + setFormState((prev) => ({ + ...prev, + endStreet: value, + endCity: "", + endState: "", + })); + } else { + setFormState((prev) => ({ + ...prev, + [name]: value, + })); + } }; const handleSubmit = async (e: React.FormEvent) => { @@ -250,12 +259,10 @@ const TripForm: React.FC<{ onTripAdded?: () => void }> = ({ onTripAdded }) => { ) return; - const startLocation = `${ - startStreet ? startStreet + ", " : "" - }${startCity}, ${startState}`; - const endLocation = `${ - endStreet ? endStreet + ", " : "" - }${endCity}, ${endState}`; + const startLocation = `${startStreet ? startStreet + ", " : "" + }${startCity}, ${startState}`; + const endLocation = `${endStreet ? endStreet + ", " : "" + }${endCity}, ${endState}`; try { const result = await addTrip({ @@ -280,6 +287,8 @@ const TripForm: React.FC<{ onTripAdded?: () => void }> = ({ onTripAdded }) => { endState: "", vehicleId: "", departureDate: getTodayDate(), + startLocation: "", + endLocation: "", }); } catch (err) { console.error("Error adding trip:", err); @@ -298,144 +307,147 @@ const TripForm: React.FC<{ onTripAdded?: () => void }> = ({ onTripAdded }) => { } return ( - Add Trip
} - > -
- {vehicleLoading ? ( -

Loading vehicles...

- ) : ( -
- -
- )} - -
-
-

- Start Location -

- - - -
- -
-

- End Location -

- - - -
-
-
- -
- - - - {error &&

Error: {error.message}

} -
- - {confirmation && ( -
-

- Trip Added Successfully! -

-

- Miles:{" "} - {confirmation.miles.toLocaleString(undefined, { - minimumFractionDigits: 2, - maximumFractionDigits: 2, - })} -

-

- Weather at Destination: {weatherEmoji}{" "} - {weatherDesc} {weatherTemp} -

-
+ <> + setGoogleMapsLoaded(true)} /> + {googleMapsLoaded && ( + Add Trip}> +
+ {vehicleLoading ? ( +

Loading vehicles...

+ ) : ( +
+ +
+ )} + +
+
+

+ Start Location +

+ + + +
+ +
+

+ End Location +

+ + + +
+
+
+ +
+ + + + {error &&

Error: {error.message}

} +
+ + {confirmation && ( +
+

+ Trip Added Successfully! +

+

+ Miles:{" "} + {confirmation.miles.toLocaleString(undefined, { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + })} +

+

+ Weather at Destination: {weatherEmoji}{" "} + {weatherDesc} {weatherTemp} +

+
+ )} +
)} -
+ ); };