diff --git a/components/sections/Offer1.js b/components/sections/Offer1.js
index 6e2e7a7..8b1fcba 100644
--- a/components/sections/Offer1.js
+++ b/components/sections/Offer1.js
@@ -57,7 +57,7 @@ export default function Offer1() {
<>
{/*-============spacing==========-*/}
-
+
{/*-============spacing==========-*/}
@@ -143,7 +143,7 @@ export default function Offer1() {
{/*-============spacing==========-*/}
-
+
{/*-============spacing==========-*/}
diff --git a/components/sections/ZipvanQuote.js b/components/sections/ZipvanQuote.js
new file mode 100644
index 0000000..ced12e5
--- /dev/null
+++ b/components/sections/ZipvanQuote.js
@@ -0,0 +1,421 @@
+// src/components/ZipvanQuote.js
+import React, { useEffect, useRef, useState } from "react";
+
+export default function ZipvanQuote() {
+ const CONFIG = {
+ GOOGLE_MAPS_API_KEY:
+ process.env.NEXT_PUBLIC_GOOGLE_MAPS_KEY ||
+ "AIzaSyAxWCXjM3_iYRKdWPUceRo79DlvIU9xnZQ",
+ CALENDLY_EVENT_URL: "https://calendly.com/zipvan/new-meeting",
+ PUBLIC_RATES: { booking_flat: 25, km_rate_under_equal_15: 3, km_rate_over_15: 2 },
+ TAX_RATE: 0.13,
+ BOUNDS: { SW: { lat: 42.8, lng: -81.0 }, NE: { lat: 44.3, lng: -78.5 } },
+ };
+
+ const pickupRef = useRef(null);
+ const dropoffRef = useRef(null);
+ const mapRef = useRef(null);
+ const schedRef = useRef(null);
+
+ const mapObj = useRef(null);
+ const dirSvc = useRef(null);
+ const dirRenderer = useRef(null);
+ const geocoder = useRef(null);
+ const markerP = useRef(null);
+ const markerD = useRef(null);
+ const routeToken = useRef(0);
+
+ const [quote, setQuote] = useState({
+ km: 0,
+ min: 0,
+ subtotal: 0,
+ tax: 0,
+ grand: 0,
+ });
+ const [mapsReady, setMapsReady] = useState(false);
+ const [mapsError, setMapsError] = useState(null);
+
+ const money = (n) => "$" + Number(n).toFixed(2);
+ const q = (p) =>
+ Object.entries(p)
+ .filter(([, v]) => v !== "" && v != null)
+ .map(([k, v]) => encodeURIComponent(k) + "=" + encodeURIComponent(v))
+ .join("&");
+ const showMapStatus = (m) => {
+ const el = document.getElementById("mapStatus");
+ if (el) {
+ el.style.display = "block";
+ el.textContent = m;
+ }
+ };
+ const debounce = (fn, wait = 700) => {
+ let t;
+ return (...a) => {
+ clearTimeout(t);
+ t = setTimeout(() => fn(...a), wait);
+ };
+ };
+
+ function loadGoogleMaps() {
+ return new Promise((resolve, reject) => {
+ if (window.google && window.google.maps && window.google.maps.places) {
+ resolve(window.google);
+ return;
+ }
+ if (document.getElementById("zipvan-google-maps")) {
+ const existing = document.getElementById("zipvan-google-maps");
+ existing.addEventListener("load", () => {
+ if (window.google) resolve(window.google);
+ else reject(new Error("Google loaded but window.google missing"));
+ });
+ existing.addEventListener("error", () => reject(new Error("Google Maps script error")));
+ return;
+ }
+ const s = document.createElement("script");
+ s.id = "zipvan-google-maps";
+ s.src =
+ "https://maps.googleapis.com/maps/api/js?key=" +
+ encodeURIComponent(CONFIG.GOOGLE_MAPS_API_KEY) +
+ "&libraries=places&v=weekly";
+ s.async = true;
+ s.defer = true;
+ s.onload = () => {
+ if (window.google) resolve(window.google);
+ else reject(new Error("Google Maps loaded but window.google missing"));
+ };
+ s.onerror = () => reject(new Error("Google Maps failed to load (network or invalid key)"));
+ document.head.appendChild(s);
+ });
+ }
+
+ function calcAndSetTotals({ km, min }) {
+ const rate = km <= 15 ? CONFIG.PUBLIC_RATES.km_rate_under_equal_15 : CONFIG.PUBLIC_RATES.km_rate_over_15;
+ const distanceCost = km * rate;
+ const subtotal = CONFIG.PUBLIC_RATES.booking_flat + distanceCost;
+ const tax = +(subtotal * CONFIG.TAX_RATE).toFixed(2);
+ const grand = +(subtotal + tax).toFixed(2);
+
+ setQuote({
+ km: +km.toFixed(2),
+ min: Math.round(min),
+ subtotal: +subtotal.toFixed(2),
+ tax,
+ grand,
+ });
+ }
+
+ function openCalendly() {
+ const url =
+ CONFIG.CALENDLY_EVENT_URL +
+ "?" +
+ q({
+ hide_event_type_details: 1,
+ background_color: "ffffff",
+ text_color: "0a0a0a",
+ a1: pickupRef.current ? pickupRef.current.value : "",
+ a2: dropoffRef.current ? dropoffRef.current.value : "",
+ a3: quote.km ? quote.km.toFixed(1) + " km" : "",
+ a4: money(quote.subtotal),
+ a5: money(quote.grand),
+ _cb: Date.now(),
+ });
+
+ const holder = schedRef.current ?? document.getElementById("sched");
+ if (!holder) return;
+ const existing = document.getElementById("calendly-embed-iframe");
+ if (existing) existing.src = url;
+ else {
+ const ifr = document.createElement("iframe");
+ ifr.id = "calendly-embed-iframe";
+ ifr.src = url;
+ ifr.allow = "payment *; clipboard-write *";
+ ifr.style.width = "100%";
+ ifr.style.minHeight = "980px";
+ ifr.style.border = "0";
+ holder.appendChild(ifr);
+ }
+ holder.scrollIntoView({ behavior: "smooth" });
+ }
+
+ function initMap() {
+ if (!window.google) {
+ setMapsError("Google Maps not available.");
+ return;
+ }
+ if (!mapRef.current) {
+ setMapsError("Map container not found.");
+ return;
+ }
+
+ mapObj.current = new window.google.maps.Map(mapRef.current, {
+ center: { lat: 43.65, lng: -79.38 },
+ zoom: 10,
+ gestureHandling: "greedy",
+ });
+
+ dirSvc.current = new window.google.maps.DirectionsService();
+ dirRenderer.current = new window.google.maps.DirectionsRenderer({ suppressMarkers: true, preserveViewport: false });
+ geocoder.current = new window.google.maps.Geocoder();
+ dirRenderer.current.setMap(mapObj.current);
+
+ const biasBounds = new window.google.maps.LatLngBounds(CONFIG.BOUNDS.SW, CONFIG.BOUNDS.NE);
+
+ const acOpts = {
+ types: ["address"],
+ componentRestrictions: { country: "ca" },
+ bounds: biasBounds,
+ strictBounds: false,
+ fields: ["formatted_address", "geometry"],
+ };
+
+ if (pickupRef.current) {
+ const acP = new window.google.maps.places.Autocomplete(pickupRef.current, acOpts);
+ acP.addListener("place_changed", () => handlePlace(acP, "pickup"));
+ }
+ if (dropoffRef.current) {
+ const acD = new window.google.maps.places.Autocomplete(dropoffRef.current, acOpts);
+ acD.addListener("place_changed", () => handlePlace(acD, "dropoff"));
+ }
+
+ const onBlurPick = () => geocodeManual("pickup");
+ const onBlurDrop = () => geocodeManual("dropoff");
+ pickupRef.current && pickupRef.current.addEventListener("blur", onBlurPick);
+ dropoffRef.current && dropoffRef.current.addEventListener("blur", onBlurDrop);
+
+ const geocodeDebounced = debounce((which) => {
+ const el = which === "pickup" ? pickupRef.current : dropoffRef.current;
+ const val = el ? el.value.trim() : "";
+ if (!val) return;
+ geocoder.current.geocode({ address: val, componentRestrictions: { country: "CA" }, bounds: biasBounds }, (res, st) => {
+ if (st === "OK" && res && res[0]) {
+ dropMarker(which, res[0].geometry.location, res[0].formatted_address);
+ requestRoute();
+ }
+ });
+ }, 400);
+
+ function geocodeManual(which) {
+ geocodeDebounced(which);
+ }
+
+ function handlePlace(ac, which) {
+ const p = ac.getPlace();
+ if (!p || !p.geometry) {
+ geocodeManual(which);
+ return;
+ }
+ dropMarker(which, p.geometry.location, p.formatted_address || (which === "pickup" ? pickupRef.current.value : dropoffRef.current.value));
+ requestRoute();
+ }
+
+ function dropMarker(which, latlng, label) {
+ const L = which === "pickup" ? "P" : "D";
+ const opts = {
+ position: latlng,
+ map: mapObj.current,
+ label: { text: L, color: "#fff", fontWeight: "700" },
+ title: (which === "pickup" ? "Pick up" : "Drop off") + ": " + label,
+ };
+ if (which === "pickup") {
+ if (markerP.current) markerP.current.setMap(null);
+ markerP.current = new window.google.maps.Marker(opts);
+ } else {
+ if (markerD.current) markerD.current.setMap(null);
+ markerD.current = new window.google.maps.Marker(opts);
+ }
+ }
+
+ function requestRoute() {
+ if (!markerP.current || !markerD.current) return;
+ const my = ++routeToken.current;
+ dirSvc.current.route(
+ {
+ origin: markerP.current.getPosition(),
+ destination: markerD.current.getPosition(),
+ travelMode: window.google.maps.TravelMode.DRIVING,
+ },
+ (res, st) => {
+ if (my !== routeToken.current) return;
+ if (st === "OK" && res?.routes?.[0]?.legs?.[0]) {
+ dirRenderer.current.setDirections(res);
+ const leg = res.routes[0].legs[0];
+ const km = +(leg.distance.value / 1000).toFixed(2);
+ const mins = Math.round(leg.duration.value / 60);
+ const mapStatusEl = document.getElementById("mapStatus");
+ if (mapStatusEl) mapStatusEl.style.display = "none";
+ const b = new window.google.maps.LatLngBounds();
+ b.extend(markerP.current.getPosition());
+ b.extend(markerD.current.getPosition());
+ mapObj.current.fitBounds(b);
+ calcAndSetTotals({ km, min: mins });
+ } else {
+ showMapStatus("Couldn't compute the driving route. Check Ontario addresses.");
+ }
+ }
+ );
+ }
+
+ if (!document.getElementById("zipvan-calendly-script")) {
+ const cs = document.createElement("script");
+ cs.id = "zipvan-calendly-script";
+ cs.src = "https://assets.calendly.com/assets/external/widget.js";
+ cs.async = true;
+ document.head.appendChild(cs);
+ }
+
+ setMapsReady(true);
+
+ initMap._cleanup = () => {
+ pickupRef.current && pickupRef.current.removeEventListener("blur", onBlurPick);
+ dropoffRef.current && dropoffRef.current.removeEventListener("blur", onBlurDrop);
+ };
+ }
+
+ useEffect(() => {
+ let mounted = true;
+
+ if (!CONFIG.GOOGLE_MAPS_API_KEY) {
+ setMapsError("Google Maps API key missing. Put it into CONFIG or NEXT_PUBLIC_GOOGLE_MAPS_KEY.");
+ return;
+ }
+
+ loadGoogleMaps()
+ .then(() => {
+ if (!mounted) return;
+ initMap();
+ })
+ .catch((err) => {
+ console.error("Google Maps load failed:", err);
+ setMapsError("Google Maps failed to load. Check API key, billing and allowed referrers.");
+ showMapStatus("Google Maps failed to load. See console for details.");
+ });
+
+ return () => {
+ mounted = false;
+ try {
+ if (initMap._cleanup) initMap._cleanup();
+ if (markerP.current) markerP.current.setMap(null);
+ if (markerD.current) markerD.current.setMap(null);
+ if (dirRenderer.current) dirRenderer.current.setMap(null);
+ } catch (e) { }
+ };
+ }, []);
+
+ const canBook = Boolean(quote.km && quote.min);
+
+ return (
+
+
+
+
What We Offer
+
+
See Your All-In Price in 10 Seconds
+
Enter pickup & drop-off. Your price is locked—no fuel or weekend fees.
+
+
+
+
+
+
+
+
Book a Pickup
+
Transparent pricing — distance + booking
+
+
+
+
+
+
+
+
+
+
Enter Pick up and Drop off to see your route, distance, and price.
+
+
+
+
+
+
+
+
+ {quote.km ? quote.km.toFixed(1) : "—"} km • {quote.min ? Math.round(quote.min) : "—"} min
+
+
+
+
+
+
Grand total: {quote.km ? money(quote.grand) : "—"}
+
+
+
+
Booking fee
{money(CONFIG.PUBLIC_RATES.booking_flat)}
+
Distance (${quote.km && quote.km <= 15 ? CONFIG.PUBLIC_RATES.km_rate_under_equal_15 : CONFIG.PUBLIC_RATES.km_rate_over_15}/km)
{quote.km ? money((quote.km) * (quote.km <= 15 ? CONFIG.PUBLIC_RATES.km_rate_under_equal_15 : CONFIG.PUBLIC_RATES.km_rate_over_15)) : money(0)}
+
Subtotal
{quote.km ? money(quote.subtotal) : money(0)}
+
HST (13%)
{quote.km ? money(quote.tax) : money(0)}
+
Grand total
{quote.km ? money(quote.grand) : money(0)}
+
+
+
Final price shown before you pay. Name, phone, and email are collected in the next step.
+
+
+
+
+
+
+
+
+
+ {mapsError && (
+
+ Map error: {mapsError}
+
+ )}
+
+ );
+}
diff --git a/pages/index.js b/pages/index.js
index 5f0fa92..e27b80e 100644
--- a/pages/index.js
+++ b/pages/index.js
@@ -14,6 +14,7 @@ import Slider2 from "@/components/sections/slider"
import Funfacts4 from "@/components/sections/Funfacts4"
import Location from "@/components/sections/Location"
import Offer1 from "@/components/sections/Offer1"
+import ZipvanQuote from "@/components/sections/ZipvanQuote"
export default function Home4() {
@@ -29,6 +30,7 @@ export default function Home4() {
{/* */}
+
{/* */}
{/* */}