corrections updated
This commit is contained in:
parent
33ae4560cd
commit
41ec9f91d1
@ -35,7 +35,7 @@ export default function MobileMenu({ handleMobileMenu, isMobileMenu }) {
|
||||
<Link href="/contact" className="theme_btn tp_one">Contact</Link>
|
||||
</div>
|
||||
<div className="single-mobile-header-info">
|
||||
<Link href="/contact" className="theme_btn">Service</Link>
|
||||
<Link href="/zipvan-quote" className="theme_btn">Service</Link>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
@ -34,7 +34,7 @@ export default function StickyHeader({ scroll, handleSearch, handleOptionalPanel
|
||||
</div>
|
||||
{/*menu icon*/}
|
||||
<div className="button">
|
||||
<Link href="/contact" className="theme_btn">
|
||||
<Link href="/zipvan-quote" className="theme_btn">
|
||||
Get A Quote
|
||||
<svg width={12} height={12} viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M1 11L11 1M11 1H3.5M11 1V8.5" stroke="white" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
|
||||
@ -17,7 +17,7 @@ export default function Footer2({ }) {
|
||||
<div className="footer_widgets_box pd_bottom_30">
|
||||
|
||||
|
||||
<div className="fwidget_title">
|
||||
<div className="fwidget_title">
|
||||
<h2 className="title color_white"> Move your stuff in a ZIP </h2>
|
||||
|
||||
</div>
|
||||
@ -53,7 +53,12 @@ export default function Footer2({ }) {
|
||||
</Link>
|
||||
</li> */}
|
||||
<li>
|
||||
<Link href="#" className="m_icon">
|
||||
<Link
|
||||
href="https://www.instagram.com/zipvan.ca?igsh=aTRvdXhhOHk4cXM0"
|
||||
className="m_icon"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<i className="fab fa-instagram" />
|
||||
</Link>
|
||||
</li>
|
||||
@ -79,7 +84,7 @@ export default function Footer2({ }) {
|
||||
<i className="fi-rr-arrow-small-right color_white" />
|
||||
</div>
|
||||
{/* <Link className="links color_white" href="#"> */}
|
||||
Greater Toronto Area
|
||||
Greater Toronto Area
|
||||
{/* </Link> */}
|
||||
</div>
|
||||
</li>
|
||||
@ -89,7 +94,7 @@ export default function Footer2({ }) {
|
||||
<i className="fi-rr-arrow-small-right color_white" />
|
||||
</div>
|
||||
{/* <Link className="links color_white" href="#"> */}
|
||||
Greater Hamilton Area
|
||||
Greater Hamilton Area
|
||||
{/* </Link> */}
|
||||
</div>
|
||||
</li>
|
||||
@ -99,8 +104,8 @@ export default function Footer2({ }) {
|
||||
<i className="fi-rr-arrow-small-right color_white" />
|
||||
</div>
|
||||
{/* <Link className="links color_white" href="#"> */}
|
||||
Kitchener-Waterloo-
|
||||
Cambridge Area
|
||||
Kitchener-Waterloo-
|
||||
Cambridge Area
|
||||
{/* </Link> */}
|
||||
</div>
|
||||
</li>
|
||||
@ -202,7 +207,7 @@ export default function Footer2({ }) {
|
||||
<li>
|
||||
<div className="d-flex align-items-center">
|
||||
<div className="icon trans me-2">
|
||||
<img src="/assets/images/shield.svg" className="img-fluid" alt="img" />
|
||||
<img src="/assets/images/shield.svg" className="img-fluid" alt="img" />
|
||||
</div>
|
||||
<span className="links color_white mb-0">
|
||||
Copyright 2025 © ZipVan. Powered by{" "}
|
||||
@ -223,7 +228,7 @@ export default function Footer2({ }) {
|
||||
<div className="icon trans">
|
||||
<i className=" fi-rr-clock" />
|
||||
</div>
|
||||
Working Hours : 24/7 365 Days a Year
|
||||
Working Hours : 24/7 365 Days a Year
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@ -104,7 +104,7 @@ export default function Header1({ handleSearch, handleOptionalPanel, handleMobil
|
||||
</div>
|
||||
{/*menu icon*/}
|
||||
<div className="button">
|
||||
<Link href="/contact" className="theme_btn">
|
||||
<Link href="/zipvan-quote" className="theme_btn">
|
||||
Get A Quote
|
||||
<svg width={12} height={12} viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M1 11L11 1M11 1H3.5M11 1V8.5" stroke="white" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
|
||||
@ -105,7 +105,7 @@ export default function Header2({ handleSearch, handleOptionalPanel, handleMobil
|
||||
<span className="line" />
|
||||
</div>
|
||||
<div className="button">
|
||||
<Link href="/contact" className="theme_btn">
|
||||
<Link href="/zipvan-quote" className="theme_btn">
|
||||
Get A Quote
|
||||
<svg width={12} height={12} viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M1 11L11 1M11 1H3.5M11 1V8.5" stroke="white" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
|
||||
@ -34,7 +34,7 @@ export default function Header4({ handleSearch, handleOptionalPanel, handleMobil
|
||||
</div>
|
||||
{/*menu icon*/}
|
||||
<div className="button">
|
||||
<Link href="/contact" className="theme_btn">
|
||||
<Link href="/zipvan-quote" className="theme_btn">
|
||||
Get A Quote
|
||||
<svg width={12} height={12} viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M1 11L11 1M11 1H3.5M11 1V8.5" stroke="white" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
|
||||
@ -22,7 +22,7 @@ export default function Banner1() {
|
||||
<section className="single_banner style_one">
|
||||
<div className="image_bg">
|
||||
<img
|
||||
src="/assets/images/home/banner/map-orange.webp"
|
||||
src="/assets/images/slider/banner-single-4-bg.png"
|
||||
className="img-fluid"
|
||||
alt="img"
|
||||
/>
|
||||
@ -72,7 +72,7 @@ export default function Banner1() {
|
||||
<section className="single_banner style_one">
|
||||
<div className="image_bg">
|
||||
<img
|
||||
src="/assets/images/home/banner/map-blue.webp"
|
||||
src="/assets/images/slider/banner-single-4-bg.png"
|
||||
className="img-fluid"
|
||||
alt="img"
|
||||
/>
|
||||
|
||||
@ -12,9 +12,9 @@ export default function Contact() {
|
||||
<div className="row">
|
||||
<div className="col-lg-12">
|
||||
<div className="section_title text-center type_five">
|
||||
<h4 className="sm_title">Areas we serve</h4>
|
||||
<h4 className="sm_title">Areas We Serve</h4>
|
||||
<div className="title_whole">
|
||||
<h2 className="title">Serving Cities Across Ontario</h2>
|
||||
<h2 className="title">Serving Cities Across Southern Ontario</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -319,7 +319,7 @@ export default function ZipvanQuote() {
|
||||
--zip-orange:#ff6500; --zip-black:#0a0a0a; --ink:#0f1720; --muted:#6b7280;
|
||||
--surface:#ffffff; --border:#e6e8eb; --radius:16px;
|
||||
}
|
||||
.zip-wrap{max-width:1100px;margin:0 auto;padding:28px}
|
||||
.zip-wrap{max-width:1100px;margin:0 auto;}
|
||||
.card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);
|
||||
box-shadow:0 10px 28px rgba(15,23,32,.07);overflow:hidden}
|
||||
.card-head{padding:20px 22px;background:var(--zip-black);color:#fff;display:flex;justify-content:space-between;align-items:center}
|
||||
|
||||
@ -106,7 +106,7 @@ export default function AboutUs() {
|
||||
<div className="position-relative">
|
||||
<ul className="list_box list color_two">
|
||||
<li>
|
||||
<div className="d-flex align-items-center">
|
||||
<div className="d-flex align-items-start">
|
||||
<div className="icon trans">
|
||||
<i aria-hidden="false" className="fas fa-check-circle" /></div>
|
||||
<p>
|
||||
@ -114,7 +114,7 @@ export default function AboutUs() {
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div className="d-flex align-items-center">
|
||||
<div className="d-flex align-items-start">
|
||||
<div className="icon trans">
|
||||
<i aria-hidden="false" className="fas fa-check-circle" /></div>
|
||||
<p>
|
||||
@ -132,10 +132,9 @@ export default function AboutUs() {
|
||||
{/*-============spacing==========-*/}
|
||||
<div className="pd_bottom_30" />
|
||||
{/*-============spacing==========-*/}
|
||||
<div className="theme_btn_all">
|
||||
<Link href="/contact" className="theme_btn">
|
||||
More About Us
|
||||
<span> <i className=" fi-rr-arrow-small-up" /></span>
|
||||
<div className="theme_btn_all">
|
||||
<Link href="/contact" className="theme_btn big rotate">
|
||||
More About Us <span> <i className=" fi-rr-arrow-small-up" /></span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
@ -169,7 +168,7 @@ export default function AboutUs() {
|
||||
<div className="position-relative">
|
||||
<ul className="list_box list color_two">
|
||||
<li>
|
||||
<div className="d-flex align-items-center">
|
||||
<div className="d-flex align-items-start">
|
||||
<div className="icon trans">
|
||||
<i aria-hidden="false" className="fas fa-check-circle" /></div>
|
||||
<p>
|
||||
@ -340,8 +339,8 @@ export default function AboutUs() {
|
||||
<div className="pd_bottom_40" />
|
||||
{/*-============spacing==========-*/}
|
||||
<div className="theme_btn_all">
|
||||
<Link href="/contact" className="theme_btn">
|
||||
Learn More <span><i className=" fi-rr-arrow-small-up" /></span>
|
||||
<Link href="/contact" className="theme_btn big rotate">
|
||||
Learn More <span> <i className=" fi-rr-arrow-small-up" /></span>
|
||||
</Link>
|
||||
</div>
|
||||
{/*-============spacing==========-*/}
|
||||
|
||||
@ -99,7 +99,7 @@ export default function Contact() {
|
||||
<div className="pd_top_90" />
|
||||
</section>
|
||||
|
||||
<section className="content-section bg_op_4 theme_alter-bg" style={{ backgroundImage: 'url(assets/images/wave-pat-1.png)' }}>
|
||||
<section className="content-section bg_op_4 theme_alter-bg" style={{ backgroundImage: 'url(/assets/images/wave-pat-1.png)' }}>
|
||||
{/*-============spacing==========-*/}
|
||||
<div className="pd_top_90" />
|
||||
{/*-============spacing==========-*/}
|
||||
@ -113,7 +113,7 @@ export default function Contact() {
|
||||
<div className="section_title medium type_one">
|
||||
<h4 className="sm_title"> Special Deals</h4>
|
||||
<div className="title_whole">
|
||||
<h2 className="title">Reasons to Choose Zip Van</h2>
|
||||
<h2 className="title text-white">Reasons to Choose Zip Van</h2>
|
||||
</div>
|
||||
</div>
|
||||
{/*-============spacing==========-*/}
|
||||
@ -284,7 +284,7 @@ export default function Contact() {
|
||||
{/*-============spacing==========-*/}
|
||||
</section>
|
||||
|
||||
<section className="call-to-action-section position-relative bg_op_1" style={{ backgroundImage: 'url(assets/images/call-bg-h4-min.jpg)' }}>
|
||||
<section className="call-to-action-section position-relative bg_op_1" style={{ backgroundImage: 'url(/assets/images/call-bg-h4-min.jpg)' }}>
|
||||
<div className="background_overlay bg_11 z_0" />
|
||||
{/*-============spacing==========-*/}
|
||||
<div className="pd_top_50" />
|
||||
|
||||
@ -102,7 +102,7 @@ export default function Contact() {
|
||||
</div>
|
||||
<div className="contact-infor">
|
||||
<h6 className="title_no_a_24">Location</h6>
|
||||
<span>55 Main Street, 2nd Block melbourne, Australia</span>
|
||||
<span>Southern Ontario, Canada</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -155,7 +155,12 @@ export default function Contact() {
|
||||
<div className="social-icons">
|
||||
<ul>
|
||||
<li>
|
||||
<Link href="#" className="m_icon">
|
||||
<Link
|
||||
href="https://www.instagram.com/zipvan.ca?igsh=aTRvdXhhOHk4cXM0"
|
||||
className="m_icon"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<i className="fab fa-instagram" />
|
||||
</Link>
|
||||
</li>
|
||||
@ -256,9 +261,9 @@ export default function Contact() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="ab_img_left_bottom z_0 mr_top_minus_150">
|
||||
{/* <div className="ab_img_left_bottom z_0 mr_top_minus_150">
|
||||
<img src="/assets/images/bg-1.png" className="img-fluid" alt="img" />
|
||||
</div>
|
||||
</div> */}
|
||||
<div className="pd_bottom_90" />
|
||||
</section>
|
||||
|
||||
@ -269,7 +274,7 @@ export default function Contact() {
|
||||
<section className="map-section">
|
||||
<div className="map-outer">
|
||||
<iframe
|
||||
src="https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d2643.6895046810805!2d-122.52642526124438!3d38.00014098339506!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x8085976736097a2f%3A0xbe014d20e6e22654!2sSan%20Rafael%2C%20California!5e0!3m2!1sen!2s!4v1678975266976!5m2!1sen!2s"
|
||||
src="https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d7330270.013314212!2d-90.91912208724011!3d43.44971233982256!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x882b5e9a9f7337f5%3A0x37ce96e31b509179!2sSouthern%20Ontario%2C%20ON!5e0!3m2!1sen!2sca!4v1694464454895!5m2!1sen!2sca"
|
||||
height={570}
|
||||
style={{ border: 0, width: "100%" }}
|
||||
allowFullScreen
|
||||
|
||||
430
pages/zipvan-quote.js
Normal file
430
pages/zipvan-quote.js
Normal file
@ -0,0 +1,430 @@
|
||||
import Layout from "@/components/layout/Layout"
|
||||
import dynamic from 'next/dynamic'
|
||||
import Link from "next/link"
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
const CounterUp = dynamic(() => import('@/components/elements/CounterUp'), {
|
||||
ssr: false,
|
||||
})
|
||||
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 (
|
||||
<>
|
||||
<Layout breadcrumbTitle="Get A Quote" footerStyle={2} background="/assets/images/contact/quote-banner.webp">
|
||||
<div className="zip-wrap" style={{ maxWidth: 1100, margin: "0 auto" }}>
|
||||
<div className="pd_top_90" />
|
||||
<div className="section_title type_four text-center">
|
||||
<h4 className="sm_title">Fast Checkout</h4>
|
||||
<div className="title_whole">
|
||||
<h2 className="title">See Your All-In Price in 10 Seconds</h2>
|
||||
<p>Enter pickup & drop-off. Your price is locked—no fuel or weekend fees.</p>
|
||||
</div>
|
||||
<div className="pd_bottom_40" />
|
||||
</div>
|
||||
<style>{`
|
||||
:root{
|
||||
--zip-orange:#ff6500; --zip-black:#0a0a0a; --ink:#0f1720; --muted:#6b7280;
|
||||
--surface:#ffffff; --border:#e6e8eb; --radius:16px;
|
||||
}
|
||||
.zip-wrap{max-width:1100px;margin:0 auto;}
|
||||
.card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);
|
||||
box-shadow:0 10px 28px rgba(15,23,32,.07);overflow:hidden}
|
||||
.card-head{padding:20px 22px;background:var(--zip-black);color:#fff;display:flex;justify-content:space-between;align-items:center}
|
||||
.card-head h2{margin:0;font-size:22px}
|
||||
.card-body{padding:22px}
|
||||
.addr-row{display:grid;grid-template-columns:1fr 1fr;gap:14px;align-items:start}
|
||||
@media(max-width:760px){ .addr-row{grid-template-columns:1fr} }
|
||||
.input{width:100%;padding:12px 14px;border:1px solid var(--border);border-radius:12px;
|
||||
font-size:16px;background:#fff;color:var(--ink);min-height:46px;box-sizing:border-box}
|
||||
.input:focus{outline:0;border-color:#cfd3d8;box-shadow:0 0 0 3px rgba(255,101,0,.12)}
|
||||
.map{height:390px;border-radius:12px;overflow:hidden;border:1px solid var(--border);
|
||||
background:#f6f7f8;display:flex;align-items:center;justify-content:center;margin-top:14px}
|
||||
.map .err{color:var(--muted);font-size:14px}
|
||||
.stats{display:grid;grid-template-columns:repeat(3,1fr);gap:12px;margin-top:14px}
|
||||
.stat{border:1px solid var(--border);border-radius:12px;background:#fff;padding:12px 14px}
|
||||
.stat label{display:block;font-size:12px;color:var(--muted);margin-bottom:6px}
|
||||
.stat .value{font-size:16px;font-weight:750;color:var(--ink)}
|
||||
.totalbar{display:flex;align-items:center;gap:18px;margin-top:16px}
|
||||
.grand{margin-left:auto;font-size:22px;font-weight:850}
|
||||
.breakdown{margin-top:12px;border:1px solid var(--border);border-radius:12px;overflow:hidden}
|
||||
.brow{display:flex;justify-content:space-between;padding:10px 12px;font-size:14px;background:#fff}
|
||||
.brow:nth-child(odd){background:#fbfbfc}
|
||||
.brow .lbl{color:#6b7280}
|
||||
.brow.total{font-weight:800;border-top:1px solid var(--border)}
|
||||
.hint{font-size:12px;color:#6b7280;margin-top:8px}
|
||||
.cta{display:flex;justify-content:flex-end;margin-top:16px}
|
||||
.btn{appearance:none;border:0;background:var(--zip-orange);color:#fff;font-weight:850;letter-spacing:.2px;
|
||||
padding:12px 18px;border-radius:999px;font-size:16px;cursor:pointer;transition:.15s transform,.2s opacity}
|
||||
.btn[disabled]{background:#c9ccd1;cursor:not-allowed;opacity:.85}
|
||||
.btn:active{transform:translateY(1px)}
|
||||
#sched{margin-top:26px}
|
||||
#calendly-embed-iframe{width:100%;min-height:980px;border:0}
|
||||
`}</style>
|
||||
|
||||
<div className="card">
|
||||
<div className="card-head">
|
||||
<h2 className="text-white">Book a Pickup</h2>
|
||||
<div style={{ opacity: ".9", fontSize: 13 }}>Transparent pricing — distance + booking</div>
|
||||
</div>
|
||||
|
||||
<div className="card-body">
|
||||
<div className="addr-row">
|
||||
<input id="pickup" ref={pickupRef} className="input" type="text" placeholder="Pick up address (Canada)" aria-label="Pick up" autoComplete="off" />
|
||||
<input id="dropoff" ref={dropoffRef} className="input" type="text" placeholder="Drop off address (Canada)" aria-label="Drop off" autoComplete="off" />
|
||||
</div>
|
||||
|
||||
<div id="map" ref={mapRef} className="map">
|
||||
<div className="err" id="mapStatus">Enter Pick up and Drop off to see your route, distance, and price.</div>
|
||||
</div>
|
||||
|
||||
<div className="stats">
|
||||
<div className="stat">
|
||||
<label>Service</label>
|
||||
<div className="value">Curbside</div>
|
||||
</div>
|
||||
<div className="stat">
|
||||
<label>Crew</label>
|
||||
<div className="value">1 mover</div>
|
||||
</div>
|
||||
<div className="stat">
|
||||
<label>Distance & Time</label>
|
||||
<div className="value">
|
||||
<span id="distanceKm">{quote.km ? quote.km.toFixed(1) : "—"}</span> km • <span id="driveMin">{quote.min ? Math.round(quote.min) : "—"}</span> min
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="totalbar">
|
||||
<div className="grand">Grand total: <span id="grandTotal">{quote.km ? money(quote.grand) : "—"}</span></div>
|
||||
</div>
|
||||
|
||||
<div className="breakdown" id="breakdownBox" style={{ display: quote.km ? "block" : "none" }}>
|
||||
<div className="brow"><div className="lbl">Booking fee</div><div id="bkFee">{money(CONFIG.PUBLIC_RATES.booking_flat)}</div></div>
|
||||
<div className="brow"><div className="lbl">Distance (<span id="rateLabel">${quote.km && quote.km <= 15 ? CONFIG.PUBLIC_RATES.km_rate_under_equal_15 : CONFIG.PUBLIC_RATES.km_rate_over_15}</span>/km)</div><div id="distAmt">{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)}</div></div>
|
||||
<div className="brow"><div className="lbl">Subtotal</div><div id="subAmt">{quote.km ? money(quote.subtotal) : money(0)}</div></div>
|
||||
<div className="brow"><div className="lbl">HST (13%)</div><div id="taxAmt">{quote.km ? money(quote.tax) : money(0)}</div></div>
|
||||
<div className="brow total"><div>Grand total</div><div id="grandAmt">{quote.km ? money(quote.grand) : money(0)}</div></div>
|
||||
</div>
|
||||
|
||||
<div className="hint">Final price shown before you pay. Name, phone, and email are collected in the next step.</div>
|
||||
|
||||
<div className="cta">
|
||||
<button id="bookNow" className="btn" disabled={!canBook} onClick={() => { if (canBook) openCalendly(); }}>
|
||||
Continue to Scheduling
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="sched" ref={schedRef}></div>
|
||||
|
||||
{mapsError && (
|
||||
<div style={{ marginTop: 12, padding: 12, background: "#fff6f6", color: "#b91c1c", borderRadius: 8 }}>
|
||||
<strong>Map error:</strong> {mapsError}
|
||||
</div>
|
||||
)}
|
||||
<div className="pd_bottom_90" />
|
||||
</div>
|
||||
</Layout>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@ -9417,11 +9417,11 @@ body div.wpforms-container-full .wpforms-form .wpforms-page-button {
|
||||
height: unset !important;
|
||||
font-weight: 500;
|
||||
color: var(--color-white);
|
||||
background: var(--color-set-one-1);
|
||||
background: #ff6600;
|
||||
border-radius: 7px;
|
||||
padding: 5px 15px;
|
||||
box-shadow: unset;
|
||||
border: 1px solid var(--color-set-one-1);
|
||||
border: 1px solid #ff6600;
|
||||
transition: 0.5s ease-in-out;
|
||||
-ms-transition: 0.5s ease-in-out;
|
||||
-moz-transition: 0.5s ease-in-out;
|
||||
@ -9433,8 +9433,8 @@ body input[type=submit]:hover,
|
||||
body button[type=submit]:hover,
|
||||
body div.wpforms-container-full .wpforms-form button[type=submit]:hover,
|
||||
body div.wpforms-container-full .wpforms-form .wpforms-page-button:hover {
|
||||
background: var(--color-set-one-2);
|
||||
border-color: var(--color-set-one-2);
|
||||
background: #08254b;
|
||||
border-color: #08254b;
|
||||
}
|
||||
|
||||
div.wpforms-container .wpforms-form .choices__inner {
|
||||
@ -15233,7 +15233,7 @@ body .wp-block-rss li a {
|
||||
|
||||
.social-icons ul li .m_icon {
|
||||
border: 0px;
|
||||
background: var(--color-set-one-3);
|
||||
background: #08254b;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
color: var(--color-white);
|
||||
@ -15249,7 +15249,7 @@ body .wp-block-rss li a {
|
||||
}
|
||||
|
||||
.social-icons ul li .m_icon:hover {
|
||||
background: var(--color-set-one-2);
|
||||
background: #36619A;
|
||||
color: var(--color-white);
|
||||
}
|
||||
|
||||
@ -17398,3 +17398,19 @@ input[type=range i]:focus::-webkit-slider-thumb {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.swiper-button-next,
|
||||
.swiper-button-prev {
|
||||
color: #ff6600 !important;
|
||||
}
|
||||
|
||||
.list_box .d-flex {
|
||||
gap: 10px;
|
||||
align-items: baseline !important;
|
||||
}
|
||||
|
||||
.list_box .icon i {
|
||||
color: #ff6600;
|
||||
font-size: 18px;
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
BIN
public/assets/images/contact/quote-banner.webp
Normal file
BIN
public/assets/images/contact/quote-banner.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
Loading…
x
Reference in New Issue
Block a user