// 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 (

Fast Checkout

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.
Curbside
1 mover
{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}
)}
); }