From a520d7a307045aaf349b94611944ed21c0f25a4a Mon Sep 17 00:00:00 2001 From: selvi Date: Mon, 8 Sep 2025 18:22:45 +0530 Subject: [PATCH] script component upadted --- components/sections/Offer1.js | 4 +- components/sections/ZipvanQuote.js | 421 +++++++++++++++++++++++++++++ pages/index.js | 2 + 3 files changed, 425 insertions(+), 2 deletions(-) create mode 100644 components/sections/ZipvanQuote.js 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.
+
+ +
+
+ +
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} +
+ )} +
+ ); +} 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() { {/* */} + {/* */} {/* */}