contact integration updated

This commit is contained in:
Selvi 2026-04-18 17:12:58 +05:30
parent 332ed5ef7f
commit 1281329360
21 changed files with 1504 additions and 893 deletions

178
app/about/AboutClient.tsx Normal file
View File

@ -0,0 +1,178 @@
"use client";
import Link from 'next/link';
import Image from 'next/image';
import { useState, useEffect } from 'react';
export default function AboutPage() {
const [testIndex, setTestIndex] = useState(0);
const [openFaq, setOpenFaq] = useState<number | null>(null);
const testimonials = [
{ quote: "VG Fence consistently delivers high-quality materials on time. Their contractor pricing allows me to stay competitive, and their inventory is unmatched.", author: "Mark S.", role: "Local Fence Contractor" },
{ quote: "Their galvanized and black finish railings are top-notch. It's rare to find a supplier that combines durability with such an aesthetic appeal.", author: "Sarah L.", role: "Property Manager" },
{ quote: "The team at VG Fence is incredibly knowledgeable. They helped us select the right ornamental fencing for our latest residential development.", author: "David K.", role: "Construction Manager" }
];
const nextTestimonial = () => setTestIndex((prev) => (prev + 1) % testimonials.length);
const prevTestimonial = () => setTestIndex((prev) => (prev - 1 + testimonials.length) % testimonials.length);
const faqs = [
{ question: "Do you offer contractor pricing?", answer: "Yes! We provide dedicated contractor accounts with specialized pricing. You need to create an account and verify your business details to unlock these rates." },
{ question: "Do you offer installation services?", answer: "While our primary focus is supplying high-quality materials, we do provide installation support for select projects based on their size and scope." },
{ question: "Where are your materials sourced from?", answer: "We source professional-grade materials focused on durability, specifically suited for Canada's diverse climate, including robust galvanized and black finish options." },
{ question: "Do you offer delivery across Ontario?", answer: "Yes, we provide reliable delivery services across Ontario for both residential and commercial projects. Delivery times depend on the order size and location." },
{ question: "Can I order custom gate sizes?", answer: "Absolutely. We specialize in custom gate fabrication. You can provide us with your specific dimensions and requirements, and we will manufacture them to fit your project perfectly." },
];
// Auto-slide effect
useEffect(() => {
const interval = setInterval(() => {
nextTestimonial();
}, 5000); // Change slide every 5 seconds
return () => clearInterval(interval);
}, [testIndex]); // Reset timer when index changes (manual navigation)
return (
<div className="about-page-wrapper">
{/* Inner Banner */}
<section className="inner-banner fade-up">
<div className="inner-banner-content">
<h1 className="section-h2">Your Trusted <span>Fencing</span> Partner</h1>
<div className="banner-breadcrumb" style={{ marginTop: '30px', marginBottom: '0' }}>
<Link href="/">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>
Home
</Link>
<span className="separator">/</span>
<span>About Us</span>
</div>
</div>
</section>
{/* Our Story Section */}
<section className="about-section section">
<div className="container about-story-container">
<div className="about-story-layout">
<div>
<div className="section-eyebrow">Our Story</div>
<h2 className="section-h2 about-story-h2">Your Trusted <span>Fencing </span>Partner</h2>
<div className="about-story-text">
<p>
At VG Fence, we are dedicated to providing high-quality fencing and railing materials to contractors, property managers, event organizers, and homeowners. With years of experience in the industry, we have built a reputation as a reliable supplier for both commercial and residential projects.
</p>
<p>
We stock and supply a comprehensive range of fence products, including chain link fences, temporary fencing, ornamental fences, wood fence hardware, gates, railings, deck products, and privacy screens. All our materials are available in galvanized and black finishes, ensuring durability and long-lasting performance.
</p>
<p>
While our focus is primarily on material supply, we also provide installation support for select projects, based on size and scope.
</p>
<div style={{ marginTop: '40px' }}>
<Link href="/products" className="btn-primary">Learn More</Link>
</div>
</div>
</div>
<div className="about-image-column">
<div className="about-image-wrapper">
<Image
src="/assets/about-fencing.png"
alt="Quality modern fencing in a beautiful garden"
fill
className="about-image-cover"
/>
</div>
<div className="floating-card about-floating-card" style={{ padding: '30px', maxWidth: '450px' }}>
<div className="section-eyebrow" style={{ color: 'var(--orange)', marginBottom: '12px' }}>Our Mission</div>
<div className="about-floating-card-text" style={{ fontSize: '15px', lineHeight: '1.6', fontWeight: '500' }}>
To supply premium fencing and railing materials that combine quality, durability, and convenience, helping every project large or small succeed.
</div>
</div>
</div>
</div>
</div>
</section>
{/* Mission Section */}
{/* <section className="section about-mission-section">
<div className="container about-mission-container">
<div className="section-eyebrow about-mission-eyebrow">Our Mission</div>
<h2 className="section-h2 about-mission-h2">Empowering <span>Success.</span></h2>
<div className="about-mission-card">
<p className="about-mission-text">
"To supply premium fencing and railing materials that combine quality, durability, and convenience,
helping every project large or small succeed."
</p>
</div>
</div>
</section> */}
{/* Testimonial Section (Slider) */}
<section className="section testimonials-section">
<div className="container about-testimonial-container">
<div className="section-eyebrow about-testimonial-eyebrow">Testimonials</div>
<h2 className="section-h2 about-testimonial-h2">What Our Partners <span>Say.</span></h2>
<div className="testimonials-slider">
<div className="slider-inner" style={{ transform: `translateX(-${testIndex * 100}%)` }}>
{testimonials.map((test, idx) => (
<div className="slide" key={idx}>
<div className="testimonial-card about-testimonial-card-inner">
<div className="testimonial-quote about-testimonial-quote">{test.quote}</div>
<div className="testimonial-author about-testimonial-author">{test.author}</div>
<div className="testimonial-role about-testimonial-role">{test.role}</div>
</div>
</div>
))}
</div>
<div className="slider-controls">
<button className="slider-btn" onClick={prevTestimonial}>&#8592;</button>
<button className="slider-btn" onClick={nextTestimonial}>&#8594;</button>
</div>
</div>
</div>
</section>
{/* FAQ Section (Accordion) */}
<section className="section faq-section">
<div className="container about-faq-container">
<div className="section-eyebrow about-faq-eyebrow">FAQ</div>
<h2 className="section-h2 about-faq-h2">Frequently Asked <span>Questions.</span></h2>
<div className="faq-accordion">
{faqs.map((faq, idx) => (
<div className="faq-item" key={idx}>
<div
className="faq-header"
onClick={() => setOpenFaq(openFaq === idx ? null : idx)}
>
<div className="faq-question">{faq.question}</div>
<div className={`faq-icon ${openFaq === idx ? 'open' : ''}`}>
&#9662;
</div>
</div>
<div className={`faq-answer-collapse ${openFaq === idx ? 'open' : ''}`}>
<div className="faq-answer-inner">
{faq.answer}
</div>
</div>
</div>
))}
</div>
</div>
</section>
{/* CTA Section */}
{/* <section className="cta-section section-padding about-cta-section">
<h2 className="cta-h2">Ready to <span>Build?</span></h2>
<p className="cta-sub about-cta-sub">Explore Ontario's most reliable inventory of fencing products.</p>
<div className="cta-btns">
<Link href="/products" className="btn-primary">View Full Catalog</Link>
<Link href="/contact" className="btn-white-outline">Contact Us</Link>
</div>
</section> */}
</div>
);
}

View File

@ -1,178 +1,14 @@
"use client"; import type { Metadata } from "next";
import AboutClient from "./AboutClient";
import Link from 'next/link'; export const metadata: Metadata = {
import Image from 'next/image'; title: "About Us | VG Fence Products",
import { useState, useEffect } from 'react'; description: "Learn about Ontario's leading B2B supplier for fencing and railing products. Quality, durability, and expert service.",
alternates: {
canonical: "/about"
}
};
export default function AboutPage() { export default function AboutPage() {
const [testIndex, setTestIndex] = useState(0); return <AboutClient />;
const [openFaq, setOpenFaq] = useState<number | null>(null);
const testimonials = [
{ quote: "VG Fence consistently delivers high-quality materials on time. Their contractor pricing allows me to stay competitive, and their inventory is unmatched.", author: "Mark S.", role: "Local Fence Contractor" },
{ quote: "Their galvanized and black finish railings are top-notch. It's rare to find a supplier that combines durability with such an aesthetic appeal.", author: "Sarah L.", role: "Property Manager" },
{ quote: "The team at VG Fence is incredibly knowledgeable. They helped us select the right ornamental fencing for our latest residential development.", author: "David K.", role: "Construction Manager" }
];
const nextTestimonial = () => setTestIndex((prev) => (prev + 1) % testimonials.length);
const prevTestimonial = () => setTestIndex((prev) => (prev - 1 + testimonials.length) % testimonials.length);
const faqs = [
{ question: "Do you offer contractor pricing?", answer: "Yes! We provide dedicated contractor accounts with specialized pricing. You need to create an account and verify your business details to unlock these rates." },
{ question: "Do you offer installation services?", answer: "While our primary focus is supplying high-quality materials, we do provide installation support for select projects based on their size and scope." },
{ question: "Where are your materials sourced from?", answer: "We source professional-grade materials focused on durability, specifically suited for Canada's diverse climate, including robust galvanized and black finish options." },
{ question: "Do you offer delivery across Ontario?", answer: "Yes, we provide reliable delivery services across Ontario for both residential and commercial projects. Delivery times depend on the order size and location." },
{ question: "Can I order custom gate sizes?", answer: "Absolutely. We specialize in custom gate fabrication. You can provide us with your specific dimensions and requirements, and we will manufacture them to fit your project perfectly." },
];
// Auto-slide effect
useEffect(() => {
const interval = setInterval(() => {
nextTestimonial();
}, 5000); // Change slide every 5 seconds
return () => clearInterval(interval);
}, [testIndex]); // Reset timer when index changes (manual navigation)
return (
<div className="about-page-wrapper">
{/* Inner Banner */}
<section className="inner-banner fade-up">
<div className="inner-banner-content">
<h1 className="section-h2">Your Trusted <span>Fencing</span> Partner</h1>
<div className="banner-breadcrumb" style={{ marginTop: '30px', marginBottom: '0' }}>
<Link href="/">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>
Home
</Link>
<span className="separator">/</span>
<span>About Us</span>
</div>
</div>
</section>
{/* Our Story Section */}
<section className="about-section section">
<div className="container about-story-container">
<div className="about-story-layout">
<div>
<div className="section-eyebrow">Our Story</div>
<h2 className="section-h2 about-story-h2">Your Trusted <span>Fencing </span>Partner</h2>
<div className="about-story-text">
<p>
At VG Fence, we are dedicated to providing high-quality fencing and railing materials to contractors, property managers, event organizers, and homeowners. With years of experience in the industry, we have built a reputation as a reliable supplier for both commercial and residential projects.
</p>
<p>
We stock and supply a comprehensive range of fence products, including chain link fences, temporary fencing, ornamental fences, wood fence hardware, gates, railings, deck products, and privacy screens. All our materials are available in galvanized and black finishes, ensuring durability and long-lasting performance.
</p>
<p>
While our focus is primarily on material supply, we also provide installation support for select projects, based on size and scope.
</p>
<div style={{ marginTop: '40px' }}>
<Link href="/products" className="btn-primary">Learn More</Link>
</div>
</div>
</div>
<div className="about-image-column">
<div className="about-image-wrapper">
<Image
src="/assets/about-fencing.png"
alt="Quality modern fencing in a beautiful garden"
fill
className="about-image-cover"
/>
</div>
<div className="floating-card about-floating-card" style={{ padding: '30px', maxWidth: '450px' }}>
<div className="section-eyebrow" style={{ color: 'var(--orange)', marginBottom: '12px' }}>Our Mission</div>
<div className="about-floating-card-text" style={{ fontSize: '15px', lineHeight: '1.6', fontWeight: '500' }}>
To supply premium fencing and railing materials that combine quality, durability, and convenience, helping every project large or small succeed.
</div>
</div>
</div>
</div>
</div>
</section>
{/* Mission Section */}
{/* <section className="section about-mission-section">
<div className="container about-mission-container">
<div className="section-eyebrow about-mission-eyebrow">Our Mission</div>
<h2 className="section-h2 about-mission-h2">Empowering <span>Success.</span></h2>
<div className="about-mission-card">
<p className="about-mission-text">
"To supply premium fencing and railing materials that combine quality, durability, and convenience,
helping every project large or small succeed."
</p>
</div>
</div>
</section> */}
{/* Testimonial Section (Slider) */}
<section className="section testimonials-section">
<div className="container about-testimonial-container">
<div className="section-eyebrow about-testimonial-eyebrow">Testimonials</div>
<h2 className="section-h2 about-testimonial-h2">What Our Partners <span>Say.</span></h2>
<div className="testimonials-slider">
<div className="slider-inner" style={{ transform: `translateX(-${testIndex * 100}%)` }}>
{testimonials.map((test, idx) => (
<div className="slide" key={idx}>
<div className="testimonial-card about-testimonial-card-inner">
<div className="testimonial-quote about-testimonial-quote">{test.quote}</div>
<div className="testimonial-author about-testimonial-author">{test.author}</div>
<div className="testimonial-role about-testimonial-role">{test.role}</div>
</div>
</div>
))}
</div>
<div className="slider-controls">
<button className="slider-btn" onClick={prevTestimonial}>&#8592;</button>
<button className="slider-btn" onClick={nextTestimonial}>&#8594;</button>
</div>
</div>
</div>
</section>
{/* FAQ Section (Accordion) */}
<section className="section faq-section">
<div className="container about-faq-container">
<div className="section-eyebrow about-faq-eyebrow">FAQ</div>
<h2 className="section-h2 about-faq-h2">Frequently Asked <span>Questions.</span></h2>
<div className="faq-accordion">
{faqs.map((faq, idx) => (
<div className="faq-item" key={idx}>
<div
className="faq-header"
onClick={() => setOpenFaq(openFaq === idx ? null : idx)}
>
<div className="faq-question">{faq.question}</div>
<div className={`faq-icon ${openFaq === idx ? 'open' : ''}`}>
&#9662;
</div>
</div>
<div className={`faq-answer-collapse ${openFaq === idx ? 'open' : ''}`}>
<div className="faq-answer-inner">
{faq.answer}
</div>
</div>
</div>
))}
</div>
</div>
</section>
{/* CTA Section */}
{/* <section className="cta-section section-padding about-cta-section">
<h2 className="cta-h2">Ready to <span>Build?</span></h2>
<p className="cta-sub about-cta-sub">Explore Ontario's most reliable inventory of fencing products.</p>
<div className="cta-btns">
<Link href="/products" className="btn-primary">View Full Catalog</Link>
<Link href="/contact" className="btn-white-outline">Contact Us</Link>
</div>
</section> */}
</div>
);
} }

View File

@ -21,6 +21,9 @@ export async function generateMetadata({ params }: Props) {
return { return {
title: `${blog.title} | VG Fence Products`, title: `${blog.title} | VG Fence Products`,
description: blog.excerpt, description: blog.excerpt,
alternates: {
canonical: `/blog/${blog.slug}`
}
}; };
} }

View File

@ -4,7 +4,10 @@ import { blogs } from "@/data/blogs";
export const metadata = { export const metadata = {
title: "Blog | VG Fence Products", title: "Blog | VG Fence Products",
description: "Read our latest articles about fencing installation, maintenance, and product choices.", description: "Read our latest articles about fencing installation, maintenance, and product choices for your next project.",
alternates: {
canonical: "/blog"
}
}; };
export default function BlogPage() { export default function BlogPage() {

View File

@ -0,0 +1,276 @@
"use client";
import React, { useState, useEffect } from "react";
import ReCAPTCHA from "react-google-recaptcha";
import axios from "axios";
import Link from 'next/link';
const ContactClient = () => {
const [formData, setFormData] = useState({
name: "",
phone: "",
email: "",
service: "Commercial",
message: "",
});
const [formErrors, setFormErrors] = useState<any>({});
const [captchaToken, setCaptchaToken] = useState<string | null>(null);
const [alert, setAlert] = useState({ show: false, type: "", message: "" });
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
const { name, value } = e.target;
setFormData((prev) => ({ ...prev, [name]: value }));
};
const handleCaptchaChange = (token: string | null) => {
setCaptchaToken(token);
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
const errors: any = {};
if (!formData.name.trim()) errors.name = "Name is required.";
if (!formData.phone.trim()) errors.phone = "Phone is required.";
if (!formData.email.trim()) errors.email = "Email is required.";
if (!formData.service.trim()) errors.service = "Please select a service.";
if (!formData.message.trim()) errors.message = "Message is required.";
if (!captchaToken) errors.captcha = "Please verify the CAPTCHA.";
setFormErrors(errors);
if (Object.keys(errors).length > 0) return;
const emailData = {
...formData,
message: `<b>Project Type:</b> ${formData.service}<br /><br /><b>Message:</b> ${formData.message}`,
to: "info@vgfenceproducts.com",
senderName: "VG Fence Contact Page",
recaptchaToken: captchaToken,
};
setAlert({ show: true, type: "info", message: "Sending your message..." });
try {
await axios.post("https://mailserver.metatronnest.com/send", emailData, {
headers: { "Content-Type": "application/json" },
});
setAlert({
show: true,
type: "success",
message: "Thank you! Your message has been sent successfully.",
});
setFormData({
name: "",
phone: "",
email: "",
service: "Commercial",
message: "",
});
setCaptchaToken(null);
setFormErrors({});
} catch (error) {
console.error("❌ Error sending email:", error);
setAlert({
show: true,
type: "danger",
message: "Failed to send message. Please try again later.",
});
}
};
useEffect(() => {
if (alert.show && alert.type !== "info") {
const timer = setTimeout(() => {
setAlert((prev) => ({ ...prev, show: false }));
}, 5000);
return () => clearTimeout(timer);
}
}, [alert.show, alert.type]);
return (
<div style={{ background: 'var(--cream)' }}>
{/* Inner Banner */}
<section className="inner-banner fade-up">
<div className="inner-banner-content">
<h1 className="section-h2">How to <span>Contact</span> Us</h1>
<div className="banner-breadcrumb" style={{ marginTop: '30px', marginBottom: '0' }}>
<Link href="/">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>
Home
</Link>
<span className="separator">/</span>
<span>Contact</span>
</div>
</div>
</section>
{/* Contact Content */}
<section className="section">
<div className="container" style={{ maxWidth: '1200px', margin: '0 auto' }}>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1.2fr', gap: '80px' }} className="contact-layout">
{/* Left Col: Info & Map */}
<div>
<div style={{ marginBottom: '48px' }}>
<h3 className="section-h2" style={{ fontSize: '32px', marginBottom: '32px' }}>Office <span>Details.</span></h3>
<div style={{ display: 'flex', flexDirection: 'column', gap: '24px' }}>
<div style={{ display: 'flex', gap: '16px' }}>
<div style={{ width: '40px', height: '40px', background: 'var(--orange)', borderRadius: '8px', display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"/><circle cx="12" cy="10" r="3"/></svg>
</div>
<div>
<div style={{ fontWeight: 700, color: 'var(--navy)', marginBottom: '4px', fontFamily: 'var(--font-display)', textTransform: 'uppercase', fontSize: '14px', letterSpacing: '.05em' }}>Our Location</div>
<div style={{ fontSize: '15px', color: 'var(--gray-600)', lineHeight: 1.5 }}>125 Earl Thompson Rd, Ayr, ON N0B 1E0, Canada</div>
</div>
</div>
<div style={{ display: 'flex', gap: '16px' }}>
<div style={{ width: '40px', height: '40px', background: 'var(--orange)', borderRadius: '8px', display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"/></svg>
</div>
<div>
<div style={{ fontWeight: 700, color: 'var(--navy)', marginBottom: '4px', fontFamily: 'var(--font-display)', textTransform: 'uppercase', fontSize: '14px', letterSpacing: '.05em' }}>Direct Line Numbers</div>
<div style={{ fontSize: '15px', color: 'var(--gray-600)', lineHeight: 1.5 }}>+1 226-888-7999</div>
</div>
</div>
<div style={{ display: 'flex', gap: '16px' }}>
<div style={{ width: '40px', height: '40px', background: 'var(--orange)', borderRadius: '8px', display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/><polyline points="22,6 12,13 2,6"/></svg>
</div>
<div>
<div style={{ fontWeight: 700, color: 'var(--navy)', marginBottom: '4px', fontFamily: 'var(--font-display)', textTransform: 'uppercase', fontSize: '14px', letterSpacing: '.05em' }}>Our Email</div>
<div style={{ fontSize: '15px', color: 'var(--gray-600)', lineHeight: 1.5 }}>info@vgfenceproducts.com</div>
</div>
</div>
</div>
</div>
{/* Map Placeholder */}
<div style={{ borderRadius: '12px', overflow: 'hidden', border: '1px solid var(--gray-200)', height: '300px', background: 'var(--white)', position: 'relative' }}>
<iframe
src="https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d2903.9575459392213!2d-80.447551023419!3d43.29854497112028!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x882c7f0f0f0f0f0f%3A0x0!2zMTI1IEVhcmwgVGhvbXBzb24gUmQsIEF5ciwgT04gTjBCIDFFMCwgQ2FuYWRh!5e0!3m2!1sen!2sca!4v1713350000000!5m2!1sen!2sca"
width="100%"
height="100%"
style={{ border: 0 }}
allowFullScreen
loading="lazy"
></iframe>
</div>
</div>
<div className="quote-card" style={{ height: 'fit-content', background: 'var(--white)', borderRadius: '16px', padding: '30px', boxShadow: '0 20px 40px rgba(15, 36, 68, .08)', border: '1px solid var(--gray-200)' }}>
<div style={{ marginBottom: '32px' }}>
<h3 style={{ fontFamily: 'var(--font-display)', fontSize: '28px', fontWeight: 800, textTransform: 'uppercase', color: 'var(--navy)', marginBottom: '8px' }}>Send us a <span>Message.</span></h3>
<p style={{ fontSize: '14px', color: 'var(--gray-600)' }}>Fill out the form below and our team will get back to you within 24 hours.</p>
</div>
<form className="contact-form" onSubmit={handleSubmit} style={{ display: 'flex', flexDirection: 'column', gap: '20px' }}>
{alert.show && (
<div className={`alert alert-${alert.type === 'danger' ? 'danger' : (alert.type === 'info' ? 'info' : 'success')} mb-4`} style={{
padding: '12px 16px',
borderRadius: '8px',
fontSize: '14px',
background: alert.type === 'danger' ? '#fee2e2' : (alert.type === 'info' ? '#e0f2fe' : '#f0fdf4'),
color: alert.type === 'danger' ? '#991b1b' : (alert.type === 'info' ? '#075985' : '#166534'),
border: `1px solid ${alert.type === 'danger' ? '#fecaca' : (alert.type === 'info' ? '#bae6fd' : '#bbf7d0')}`
}}>
{alert.message}
</div>
)}
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '20px' }} className="form-row">
<div className="form-group">
<label style={{ display: 'block', fontSize: '12px', fontWeight: 700, textTransform: 'uppercase', color: 'var(--navy)', marginBottom: '8px' }}>Name</label>
<input
type="text"
name="name"
placeholder="John Doe"
value={formData.name}
onChange={handleChange}
style={{ width: '100%', padding: '12px 16px', borderRadius: '4px', border: formErrors.name ? '1px solid #ef4444' : '1px solid var(--gray-200)', fontSize: '14px', background: 'var(--white)' }}
/>
{formErrors.name && <small style={{ color: '#ef4444', fontSize: '11px', marginTop: '4px', display: 'block' }}>{formErrors.name}</small>}
</div>
<div className="form-group">
<label style={{ display: 'block', fontSize: '12px', fontWeight: 700, textTransform: 'uppercase', color: 'var(--navy)', marginBottom: '8px' }}>Email</label>
<input
type="email"
name="email"
placeholder="john@example.com"
value={formData.email}
onChange={handleChange}
style={{ width: '100%', padding: '12px 16px', borderRadius: '4px', border: formErrors.email ? '1px solid #ef4444' : '1px solid var(--gray-200)', fontSize: '14px', background: 'var(--white)' }}
/>
{formErrors.email && <small style={{ color: '#ef4444', fontSize: '11px', marginTop: '4px', display: 'block' }}>{formErrors.email}</small>}
</div>
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '20px' }} className="form-row">
<div className="form-group">
<label style={{ display: 'block', fontSize: '12px', fontWeight: 700, textTransform: 'uppercase', color: 'var(--navy)', marginBottom: '8px' }}>Phone Number</label>
<input
type="tel"
name="phone"
placeholder="(555) 123-4567"
value={formData.phone}
onChange={handleChange}
style={{ width: '100%', padding: '12px 16px', borderRadius: '4px', border: formErrors.phone ? '1px solid #ef4444' : '1px solid var(--gray-200)', fontSize: '14px', background: 'var(--white)' }}
/>
{formErrors.phone && <small style={{ color: '#ef4444', fontSize: '11px', marginTop: '4px', display: 'block' }}>{formErrors.phone}</small>}
</div>
<div className="form-group">
<label style={{ display: 'block', fontSize: '12px', fontWeight: 700, textTransform: 'uppercase', color: 'var(--navy)', marginBottom: '8px' }}>Project Type</label>
<select
name="service"
value={formData.service}
onChange={handleChange}
style={{ width: '100%', padding: '12px 16px', borderRadius: '4px', border: '1px solid var(--gray-200)', fontSize: '14px', background: 'var(--white)', appearance: 'none' }}
>
<option value="Commercial">Commercial</option>
<option value="Residential">Residential</option>
<option value="Event">Event</option>
<option value="Other">Other</option>
</select>
</div>
</div>
<div className="form-group">
<label style={{ display: 'block', fontSize: '12px', fontWeight: 700, textTransform: 'uppercase', color: 'var(--navy)', marginBottom: '8px' }}>Message</label>
<textarea
name="message"
placeholder="How can we help you?"
value={formData.message}
onChange={handleChange}
style={{ width: '100%', padding: '12px 16px', borderRadius: '4px', border: formErrors.message ? '1px solid #ef4444' : '1px solid var(--gray-200)', fontSize: '14px', minHeight: '150px', resize: 'vertical', background: 'var(--white)' }}
></textarea>
{formErrors.message && <small style={{ color: '#ef4444', fontSize: '11px', marginTop: '4px', display: 'block' }}>{formErrors.message}</small>}
</div>
<div className="form-group">
<ReCAPTCHA
sitekey="6LekfpwrAAAAAOTwuP1d2gg-Fv9UEsAjE2gjOQJl"
onChange={handleCaptchaChange}
/>
{formErrors.captcha && <small style={{ color: '#ef4444', fontSize: '11px', marginTop: '4px', display: 'block' }}>{formErrors.captcha}</small>}
</div>
<button type="submit" className="btn-primary" style={{ width: '100%', marginTop: '10px' }} disabled={alert.type === "info"}>
{alert.type === "info" ? "Sending..." : "Send Message →"}
</button>
</form>
</div>
</div>
</div>
</section>
</div>
);
};
export default ContactClient;

View File

@ -1,128 +1,14 @@
import Link from 'next/link'; import type { Metadata } from "next";
import Navbar from '@/components/Navbar'; import ContactClient from "./ContactClient";
import Footer from '@/components/Footer';
export const metadata: Metadata = {
title: "Contact Us | VG Fence Products",
description: "Get in touch with our expert team for quotes, product inquiries, or technical support. We are located in Ayr, Ontario and serve the entire province.",
alternates: {
canonical: "/contact"
}
};
export default function ContactPage() { export default function ContactPage() {
return ( return <ContactClient />;
<div style={{ background: 'var(--cream)' }}>
{/* Inner Banner */}
<section className="inner-banner fade-up">
<div className="inner-banner-content">
<h1 className="section-h2">How to <span>Contact</span> Us</h1>
<div className="banner-breadcrumb" style={{ marginTop: '30px', marginBottom: '0' }}>
<Link href="/">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>
Home
</Link>
<span className="separator">/</span>
<span>Contact</span>
</div>
</div>
</section>
{/* Contact Content */}
<section className="section">
<div className="container" style={{ maxWidth: '1200px', margin: '0 auto' }}>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1.2fr', gap: '80px' }} className="contact-layout">
{/* Left Col: Info & Map */}
<div>
<div style={{ marginBottom: '48px' }}>
<h3 className="section-h2" style={{ fontSize: '32px', marginBottom: '32px' }}>Office <span>Details.</span></h3>
<div style={{ display: 'flex', flexDirection: 'column', gap: '24px' }}>
<div style={{ display: 'flex', gap: '16px' }}>
<div style={{ width: '40px', height: '40px', background: 'var(--orange)', borderRadius: '8px', display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"/><circle cx="12" cy="10" r="3"/></svg>
</div>
<div>
<div style={{ fontWeight: 700, color: 'var(--navy)', marginBottom: '4px', fontFamily: 'var(--font-display)', textTransform: 'uppercase', fontSize: '14px', letterSpacing: '.05em' }}>Our Location</div>
<div style={{ fontSize: '15px', color: 'var(--gray-600)', lineHeight: 1.5 }}>125 Earl Thompson Rd, Ayr, ON N0B 1E0, Canada</div>
</div>
</div>
<div style={{ display: 'flex', gap: '16px' }}>
<div style={{ width: '40px', height: '40px', background: 'var(--orange)', borderRadius: '8px', display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"/></svg>
</div>
<div>
<div style={{ fontWeight: 700, color: 'var(--navy)', marginBottom: '4px', fontFamily: 'var(--font-display)', textTransform: 'uppercase', fontSize: '14px', letterSpacing: '.05em' }}>Direct Line Numbers</div>
<div style={{ fontSize: '15px', color: 'var(--gray-600)', lineHeight: 1.5 }}>+1 226-888-7999</div>
</div>
</div>
<div style={{ display: 'flex', gap: '16px' }}>
<div style={{ width: '40px', height: '40px', background: 'var(--orange)', borderRadius: '8px', display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/><polyline points="22,6 12,13 2,6"/></svg>
</div>
<div>
<div style={{ fontWeight: 700, color: 'var(--navy)', marginBottom: '4px', fontFamily: 'var(--font-display)', textTransform: 'uppercase', fontSize: '14px', letterSpacing: '.05em' }}>Our Email</div>
<div style={{ fontSize: '15px', color: 'var(--gray-600)', lineHeight: 1.5 }}>info@vgfenceproducts.com</div>
</div>
</div>
</div>
</div>
{/* Map Placeholder */}
<div style={{ borderRadius: '12px', overflow: 'hidden', border: '1px solid var(--gray-200)', height: '300px', background: 'var(--white)', position: 'relative' }}>
<iframe
src="https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d2903.9575459392213!2d-80.447551023419!3d43.29854497112028!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x882c7f0f0f0f0f0f%3A0x0!2zMTI1IEVhcmwgVGhvbXBzb24gUmQsIEF5ciwgT04gTjBCIDFFMCwgQ2FuYWRh!5e0!3m2!1sen!2sca!4v1713350000000!5m2!1sen!2sca"
width="100%"
height="100%"
style={{ border: 0 }}
allowFullScreen
loading="lazy"
></iframe>
</div>
</div>
<div className="quote-card" style={{ height: 'fit-content', background: 'var(--white)', borderRadius: '16px', padding: '30px', boxShadow: '0 20px 40px rgba(15, 36, 68, .08)', border: '1px solid var(--gray-200)' }}>
<div style={{ marginBottom: '32px' }}>
<h3 style={{ fontFamily: 'var(--font-display)', fontSize: '28px', fontWeight: 800, textTransform: 'uppercase', color: 'var(--navy)', marginBottom: '8px' }}>Send us a <span>Message.</span></h3>
<p style={{ fontSize: '14px', color: 'var(--gray-600)' }}>Fill out the form below and our team will get back to you within 24 hours.</p>
</div>
<form style={{ display: 'flex', flexDirection: 'column', gap: '20px' }}>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '20px' }} className="form-row">
<div className="form-group">
<label style={{ display: 'block', fontSize: '12px', fontWeight: 700, textTransform: 'uppercase', color: 'var(--navy)', marginBottom: '8px' }}>Name</label>
<input type="text" placeholder="John Doe" style={{ width: '100%', padding: '12px 16px', borderRadius: '4px', border: '1px solid var(--gray-200)', fontSize: '14px', background: 'var(--white)' }} />
</div>
<div className="form-group">
<label style={{ display: 'block', fontSize: '12px', fontWeight: 700, textTransform: 'uppercase', color: 'var(--navy)', marginBottom: '8px' }}>Email</label>
<input type="email" placeholder="john@example.com" style={{ width: '100%', padding: '12px 16px', borderRadius: '4px', border: '1px solid var(--gray-200)', fontSize: '14px', background: 'var(--white)' }} />
</div>
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '20px' }} className="form-row">
<div className="form-group">
<label style={{ display: 'block', fontSize: '12px', fontWeight: 700, textTransform: 'uppercase', color: 'var(--navy)', marginBottom: '8px' }}>Phone Number</label>
<input type="tel" placeholder="(555) 123-4567" style={{ width: '100%', padding: '12px 16px', borderRadius: '4px', border: '1px solid var(--gray-200)', fontSize: '14px', background: 'var(--white)' }} />
</div>
<div className="form-group">
<label style={{ display: 'block', fontSize: '12px', fontWeight: 700, textTransform: 'uppercase', color: 'var(--navy)', marginBottom: '8px' }}>Project Type</label>
<select style={{ width: '100%', padding: '12px 16px', borderRadius: '4px', border: '1px solid var(--gray-200)', fontSize: '14px', background: 'var(--white)', appearance: 'none' }}>
<option value="Commercial">Commercial</option>
<option value="Residential">Residential</option>
<option value="Event">Event</option>
<option value="Other">Other</option>
</select>
</div>
</div>
<div className="form-group">
<label style={{ display: 'block', fontSize: '12px', fontWeight: 700, textTransform: 'uppercase', color: 'var(--navy)', marginBottom: '8px' }}>Message</label>
<textarea placeholder="How can we help you?" style={{ width: '100%', padding: '12px 16px', borderRadius: '4px', border: '1px solid var(--gray-200)', fontSize: '14px', minHeight: '150px', resize: 'vertical', background: 'var(--white)' }}></textarea>
</div>
<button type="submit" className="btn-primary" style={{ width: '100%', marginTop: '10px' }}>Send Message </button>
</form>
</div>
</div>
</div>
</section>
</div>
);
} }

View File

@ -1469,7 +1469,7 @@ footer {
background: rgba(255, 255, 255, .03); background: rgba(255, 255, 255, .03);
border: 1px solid rgba(255, 255, 255, .1); border: 1px solid rgba(255, 255, 255, .1);
border-radius: 16px; border-radius: 16px;
padding: 48px; padding: 35px;
backdrop-filter: blur(20px); backdrop-filter: blur(20px);
position: relative; position: relative;
z-index: 2; z-index: 2;

View File

@ -17,11 +17,15 @@ const barlowCondensed = Barlow_Condensed({
}); });
export const metadata: Metadata = { export const metadata: Metadata = {
metadataBase: new URL("https://vgfenceproducts.ca"),
title: "VG Fence Products — Ontario's B2B Fence Supply Partner", title: "VG Fence Products — Ontario's B2B Fence Supply Partner",
description: "Supplying contractors, builders, and property managers across Ontario with chain link, ornamental, composite, glass railing, and stain products.", description: "Supplying contractors, builders, and property managers across Ontario with chain link, ornamental, composite, glass railing, and stain products.",
icons: { icons: {
icon: "/assets/favicon.webp", icon: "/assets/favicon.webp",
}, },
alternates: {
canonical: "/",
},
}; };
export default function RootLayout({ export default function RootLayout({

125
app/login/LoginClient.tsx Normal file
View File

@ -0,0 +1,125 @@
"use client";
import Link from 'next/link';
import { useState } from 'react';
import { useRouter } from 'next/navigation';
export default function LoginPage() {
const router = useRouter();
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [loading, setLoading] = useState(false);
const [err, setErr] = useState<string | null>(null);
const [msg, setMsg] = useState<string | null>(null);
const submitForm = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setErr(null);
setMsg(null);
if (!email || !password) {
setErr('Please enter username/email and password.');
return;
}
setLoading(true);
try {
const res = await fetch('http://localhost:3050/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
});
const contentType = res.headers.get('content-type') || '';
const data = contentType.includes('application/json') ? await res.json() : await res.text();
if (!res.ok) {
throw new Error((typeof data === 'object' && (data?.message || data?.error)) || `Login failed (${res.status})`);
}
try {
sessionStorage.setItem('USERID', data.userid);
localStorage.setItem('vgproducts_uid', data.userid);
localStorage.setItem('d4a_email', data.email);
} catch {
console.log('Error setting storage');
}
setMsg('Login successful!');
setTimeout(() => router.push('/'), 500); // Redirect to homepage or dashboard after login
} catch (e: any) {
setErr(e?.message || 'Something went wrong. Please try again.');
} finally {
setLoading(false);
}
};
return (
<div className="login-page-wrapper">
{/* ── INNER BANNER ── */}
{/* <section className="inner-banner fade-up">
<div className="inner-banner-content">
<h1 className="section-h2">Customer <span>Login</span></h1>
<div className="banner-breadcrumb" style={{ marginTop: '30px', marginBottom: '0' }}>
<Link href="/">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>
Home
</Link>
<span className="separator">/</span>
<span>Login</span>
</div>
</div>
</section> */}
<div className="auth-page section">
<div className="auth-card">
<h2 className="auth-title" style={{ fontSize: '28px', marginBottom: '32px' }}>Access Your <span>Account</span></h2>
{err && <div style={{ color: 'red', marginBottom: '10px', fontSize: '14px', background: 'rgba(255,0,0,0.1)', padding: '8px', borderRadius: '4px' }}>{err}</div>}
{msg && <div style={{ color: 'green', marginBottom: '10px', fontSize: '14px', background: 'rgba(0,255,0,0.1)', padding: '8px', borderRadius: '4px' }}>{msg}</div>}
<form className="auth-form" onSubmit={submitForm}>
<div className="form-group">
<label className="form-label">Username / Email</label>
<input
className="form-input"
type="text"
placeholder="Enter Customer Email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
</div>
<div className="form-group">
<label className="form-label">Password</label>
<input
className="form-input"
type="password"
placeholder="••••••••"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '24px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<input type="checkbox" id="remember" />
<label htmlFor="remember" style={{ fontSize: '12px', color: 'rgba(255,255,255,0.5)' }}>Remember me</label>
</div>
<a href="#" style={{ fontSize: '12px', color: 'var(--orange)', textDecoration: 'none' }}>Forgot Password?</a>
</div>
<button type="submit" className="form-submit" style={{ width: '100%' }} disabled={loading}>
{loading ? 'Logging in...' : 'Login to Portal →'}
</button>
</form>
{/* <div style={{ marginTop: '32px', textAlign: 'center', fontSize: '13px', color: 'rgba(255,255,255,0.5)' }}>
Don't have a contractor account? <br />
<Link href="/#quote" style={{ color: 'var(--orange)', fontWeight: 600, textDecoration: 'none' }}>Apply for contractor pricing</Link>
</div> */}
</div>
</div>
</div>
);
}

View File

@ -1,125 +1,14 @@
"use client"; import type { Metadata } from "next";
import Link from 'next/link'; import LoginClient from "./LoginClient";
import { useState } from 'react';
import { useRouter } from 'next/navigation'; export const metadata: Metadata = {
title: "Customer Login | VG Fence Products",
description: "Access your contractor portal to view exclusive pricing, manage orders, and track deliveries. Secure B2B portal for Ontario fencing professionals.",
alternates: {
canonical: "/login"
}
};
export default function LoginPage() { export default function LoginPage() {
const router = useRouter(); return <LoginClient />;
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [loading, setLoading] = useState(false);
const [err, setErr] = useState<string | null>(null);
const [msg, setMsg] = useState<string | null>(null);
const submitForm = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setErr(null);
setMsg(null);
if (!email || !password) {
setErr('Please enter username/email and password.');
return;
}
setLoading(true);
try {
const res = await fetch('http://localhost:3050/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
});
const contentType = res.headers.get('content-type') || '';
const data = contentType.includes('application/json') ? await res.json() : await res.text();
if (!res.ok) {
throw new Error((typeof data === 'object' && (data?.message || data?.error)) || `Login failed (${res.status})`);
}
try {
sessionStorage.setItem('USERID', data.userid);
localStorage.setItem('vgproducts_uid', data.userid);
localStorage.setItem('d4a_email', data.email);
} catch {
console.log('Error setting storage');
}
setMsg('Login successful!');
setTimeout(() => router.push('/'), 500); // Redirect to homepage or dashboard after login
} catch (e: any) {
setErr(e?.message || 'Something went wrong. Please try again.');
} finally {
setLoading(false);
}
};
return (
<div className="login-page-wrapper">
{/* ── INNER BANNER ── */}
{/* <section className="inner-banner fade-up">
<div className="inner-banner-content">
<h1 className="section-h2">Customer <span>Login</span></h1>
<div className="banner-breadcrumb" style={{ marginTop: '30px', marginBottom: '0' }}>
<Link href="/">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>
Home
</Link>
<span className="separator">/</span>
<span>Login</span>
</div>
</div>
</section> */}
<div className="auth-page section">
<div className="auth-card">
<h2 className="auth-title" style={{ fontSize: '28px', marginBottom: '32px' }}>Access Your <span>Account</span></h2>
{err && <div style={{ color: 'red', marginBottom: '10px', fontSize: '14px', background: 'rgba(255,0,0,0.1)', padding: '8px', borderRadius: '4px' }}>{err}</div>}
{msg && <div style={{ color: 'green', marginBottom: '10px', fontSize: '14px', background: 'rgba(0,255,0,0.1)', padding: '8px', borderRadius: '4px' }}>{msg}</div>}
<form className="auth-form" onSubmit={submitForm}>
<div className="form-group">
<label className="form-label">Username / Email</label>
<input
className="form-input"
type="text"
placeholder="Enter Customer Email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
</div>
<div className="form-group">
<label className="form-label">Password</label>
<input
className="form-input"
type="password"
placeholder="••••••••"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '24px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<input type="checkbox" id="remember" />
<label htmlFor="remember" style={{ fontSize: '12px', color: 'rgba(255,255,255,0.5)' }}>Remember me</label>
</div>
<a href="#" style={{ fontSize: '12px', color: 'var(--orange)', textDecoration: 'none' }}>Forgot Password?</a>
</div>
<button type="submit" className="form-submit" style={{ width: '100%' }} disabled={loading}>
{loading ? 'Logging in...' : 'Login to Portal →'}
</button>
</form>
{/* <div style={{ marginTop: '32px', textAlign: 'center', fontSize: '13px', color: 'rgba(255,255,255,0.5)' }}>
Don't have a contractor account? <br />
<Link href="/#quote" style={{ color: 'var(--orange)', fontWeight: 600, textDecoration: 'none' }}>Apply for contractor pricing</Link>
</div> */}
</div>
</div>
</div>
);
} }

View File

@ -0,0 +1,145 @@
"use client";
import Link from 'next/link';
import Image from 'next/image';
export default function ManufacturingPage() {
return (
<div className="about-page-wrapper">
{/* Inner Banner */}
<section className="inner-banner fade-up">
<div className="inner-banner-content">
<h1 className="section-h2">Job <span>Order</span> Manufacturing</h1>
<div className="banner-breadcrumb" style={{ marginTop: '30px', marginBottom: '0' }}>
<Link href="/">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>
Home
</Link>
<span className="separator">/</span>
<span>Manufacturing</span>
</div>
</div>
</section>
{/* Intro Section */}
<section className="about-section section">
<div className="container about-story-container">
<div className="about-story-layout">
<div>
<div className="section-eyebrow">Our Capabilities</div>
<h2 className="section-h2 about-story-h2">Bulk & Job Order <span>Manufacturing</span></h2>
<div className="about-story-text">
<p>
VG Fence specializes in bulk and job order manufacturing for contractors, developers, and businesses.
</p>
<p>
From residential and commercial gates to brackets or custom metal components, we handle production from start to finish ensuring high-quality, durable products delivered on time.
</p>
<div style={{ marginTop: '40px' }}>
<Link href="/contact" className="btn-primary">
Get a custom quote today for your next project
</Link>
</div>
</div>
</div>
<div className="about-image-column">
<div className="about-image-wrapper">
<Image
src="/assets/manufacturing-hero.png"
alt="Industrial metal fabrication and manufacturing"
fill
className="about-image-cover"
/>
</div>
<div className="floating-card about-floating-card">
<div className="about-floating-card-100">Fence Solutions</div>
{/* <div className="about-floating-card-text">Fence Solutions</div> */}
</div>
</div>
</div>
</div>
</section>
{/* Services Grid */}
<section className="section" style={{ background: 'var(--gray-100)' }}>
<div className="container">
<div style={{ textAlign: 'center', marginBottom: '80px' }}>
<div className="section-eyebrow" style={{ justifyContent: 'center' }}>What We Build</div>
<h2 className="section-h2">Specialized <span>Production.</span></h2>
</div>
<div className="mfg-grid">
{/* Gates */}
<div className="mfg-card">
<div style={{ padding: '12px', background: 'rgba(221,107,32,0.1)', borderRadius: '12px', width: '48px', height: '48px', display: 'flex', alignItems: 'center', justifyContent: 'center', marginBottom: '24px' }}>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="var(--orange)" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M12 3v18"/><path d="M3 12h18"/></svg>
</div>
<h3 className="section-h2">Residential & <span>Commercial Gates</span></h3>
<ul className="service-list">
<li>Sliding, swing, and decorative gates</li>
<li>Custom sizes and finishes</li>
<li>Galvanized or powder-coated for durability</li>
<li>Factory-fabricated for precise fit and fast installation</li>
</ul>
</div>
{/* Components */}
<div className="mfg-card">
<div style={{ padding: '12px', background: 'rgba(221,107,32,0.1)', borderRadius: '12px', width: '48px', height: '48px', display: 'flex', alignItems: 'center', justifyContent: 'center', marginBottom: '24px' }}>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="var(--orange)" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/><polyline points="3.27 6.96 12 12.01 20.73 6.96"/><line x1="12" y1="22.08" x2="12" y2="12"/></svg>
</div>
<h3 className="section-h2">Brackets, Rails & <span>Metal Components</span></h3>
<ul className="service-list">
<li>Fence brackets, mounting hardware, rail supports</li>
<li>Custom metal components for all types of projects</li>
<li>Bulk manufacturing for contractors and developers</li>
<li>High-quality steel with consistent dimensions</li>
</ul>
</div>
{/* Bulk Orders */}
<div className="mfg-card">
<div style={{ padding: '12px', background: 'rgba(221,107,32,0.1)', borderRadius: '12px', width: '48px', height: '48px', display: 'flex', alignItems: 'center', justifyContent: 'center', marginBottom: '24px' }}>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="var(--orange)" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M3 9h18v10a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V9Z"/><path d="m3 9 2.45-4.91A2 2 0 0 1 7.24 3h9.52a2 2 0 0 1 1.79 1.09L21 9"/><path d="M12 3v6"/></svg>
</div>
<h3 className="section-h2">Bulk & <span>Job Orders</span></h3>
<ul className="service-list">
<li>Large-scale production for commercial or municipal projects</li>
<li>Pre-assembled material bundles for faster execution</li>
<li>Optional customization for dimensions and finishes</li>
<li>Competitive pricing for bulk quantities</li>
</ul>
</div>
{/* Fabrication */}
<div className="mfg-card">
<div style={{ padding: '12px', background: 'rgba(221,107,32,0.1)', borderRadius: '12px', width: '48px', height: '48px', display: 'flex', alignItems: 'center', justifyContent: 'center', marginBottom: '24px' }}>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="var(--orange)" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76Z"/></svg>
</div>
<h3 className="section-h2">Full Fabrication <span>Works</span></h3>
<ul className="service-list">
<li>Cutting, bending, welding, and finishing</li>
<li>Custom fabrication for any size requirement</li>
<li>Assistance with finishing touches for panels and accessories</li>
<li>Ensures every product meets durability standards</li>
</ul>
</div>
</div>
</div>
</section>
{/* CTA Section */}
<section className="cta-section section-padding about-cta-section">
<h2 className="cta-h2">Get Your <span>Custom Quote</span> Today</h2>
<p className="cta-sub about-cta-sub" style={{ fontSize: '20px', marginBottom: '32px' }}>
Fill out our job order & fabrication request form or call <strong style={{ color: 'var(--orange)' }}>226-888-7999</strong> to discuss your bulk and custom fabrication needs.
</p>
<div className="cta-btns">
<Link href="/contact" className="btn-primary">Contact Us Now</Link>
<a href="tel:2268887999" className="btn-white-outline">Call Us Today</a>
</div>
</section>
</div>
);
}

View File

@ -1,145 +1,14 @@
"use client"; import type { Metadata } from "next";
import ManufacturingClient from "./ManufacturingClient";
import Link from 'next/link'; export const metadata: Metadata = {
import Image from 'next/image'; title: "Bulk & Job Order Manufacturing | VG Fence Products",
description: "Specialized metal fabrication for fencing components, gates, and custom brackets. Bulk manufacturing solutions for contractors in Ontario.",
alternates: {
canonical: "/manufacturing"
}
};
export default function ManufacturingPage() { export default function ManufacturingPage() {
return ( return <ManufacturingClient />;
<div className="about-page-wrapper">
{/* Inner Banner */}
<section className="inner-banner fade-up">
<div className="inner-banner-content">
<h1 className="section-h2">Job <span>Order</span> Manufacturing</h1>
<div className="banner-breadcrumb" style={{ marginTop: '30px', marginBottom: '0' }}>
<Link href="/">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>
Home
</Link>
<span className="separator">/</span>
<span>Manufacturing</span>
</div>
</div>
</section>
{/* Intro Section */}
<section className="about-section section">
<div className="container about-story-container">
<div className="about-story-layout">
<div>
<div className="section-eyebrow">Our Capabilities</div>
<h2 className="section-h2 about-story-h2">Bulk & Job Order <span>Manufacturing</span></h2>
<div className="about-story-text">
<p>
VG Fence specializes in bulk and job order manufacturing for contractors, developers, and businesses.
</p>
<p>
From residential and commercial gates to brackets or custom metal components, we handle production from start to finish ensuring high-quality, durable products delivered on time.
</p>
<div style={{ marginTop: '40px' }}>
<Link href="/contact" className="btn-primary">
Get a custom quote today for your next project
</Link>
</div>
</div>
</div>
<div className="about-image-column">
<div className="about-image-wrapper">
<Image
src="/assets/manufacturing-hero.png"
alt="Industrial metal fabrication and manufacturing"
fill
className="about-image-cover"
/>
</div>
<div className="floating-card about-floating-card">
<div className="about-floating-card-100">Fence Solutions</div>
{/* <div className="about-floating-card-text">Fence Solutions</div> */}
</div>
</div>
</div>
</div>
</section>
{/* Services Grid */}
<section className="section" style={{ background: 'var(--gray-100)' }}>
<div className="container">
<div style={{ textAlign: 'center', marginBottom: '80px' }}>
<div className="section-eyebrow" style={{ justifyContent: 'center' }}>What We Build</div>
<h2 className="section-h2">Specialized <span>Production.</span></h2>
</div>
<div className="mfg-grid">
{/* Gates */}
<div className="mfg-card">
<div style={{ padding: '12px', background: 'rgba(221,107,32,0.1)', borderRadius: '12px', width: '48px', height: '48px', display: 'flex', alignItems: 'center', justifyContent: 'center', marginBottom: '24px' }}>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="var(--orange)" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M12 3v18"/><path d="M3 12h18"/></svg>
</div>
<h3 className="section-h2">Residential & <span>Commercial Gates</span></h3>
<ul className="service-list">
<li>Sliding, swing, and decorative gates</li>
<li>Custom sizes and finishes</li>
<li>Galvanized or powder-coated for durability</li>
<li>Factory-fabricated for precise fit and fast installation</li>
</ul>
</div>
{/* Components */}
<div className="mfg-card">
<div style={{ padding: '12px', background: 'rgba(221,107,32,0.1)', borderRadius: '12px', width: '48px', height: '48px', display: 'flex', alignItems: 'center', justifyContent: 'center', marginBottom: '24px' }}>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="var(--orange)" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/><polyline points="3.27 6.96 12 12.01 20.73 6.96"/><line x1="12" y1="22.08" x2="12" y2="12"/></svg>
</div>
<h3 className="section-h2">Brackets, Rails & <span>Metal Components</span></h3>
<ul className="service-list">
<li>Fence brackets, mounting hardware, rail supports</li>
<li>Custom metal components for all types of projects</li>
<li>Bulk manufacturing for contractors and developers</li>
<li>High-quality steel with consistent dimensions</li>
</ul>
</div>
{/* Bulk Orders */}
<div className="mfg-card">
<div style={{ padding: '12px', background: 'rgba(221,107,32,0.1)', borderRadius: '12px', width: '48px', height: '48px', display: 'flex', alignItems: 'center', justifyContent: 'center', marginBottom: '24px' }}>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="var(--orange)" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M3 9h18v10a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V9Z"/><path d="m3 9 2.45-4.91A2 2 0 0 1 7.24 3h9.52a2 2 0 0 1 1.79 1.09L21 9"/><path d="M12 3v6"/></svg>
</div>
<h3 className="section-h2">Bulk & <span>Job Orders</span></h3>
<ul className="service-list">
<li>Large-scale production for commercial or municipal projects</li>
<li>Pre-assembled material bundles for faster execution</li>
<li>Optional customization for dimensions and finishes</li>
<li>Competitive pricing for bulk quantities</li>
</ul>
</div>
{/* Fabrication */}
<div className="mfg-card">
<div style={{ padding: '12px', background: 'rgba(221,107,32,0.1)', borderRadius: '12px', width: '48px', height: '48px', display: 'flex', alignItems: 'center', justifyContent: 'center', marginBottom: '24px' }}>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="var(--orange)" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76Z"/></svg>
</div>
<h3 className="section-h2">Full Fabrication <span>Works</span></h3>
<ul className="service-list">
<li>Cutting, bending, welding, and finishing</li>
<li>Custom fabrication for any size requirement</li>
<li>Assistance with finishing touches for panels and accessories</li>
<li>Ensures every product meets durability standards</li>
</ul>
</div>
</div>
</div>
</section>
{/* CTA Section */}
<section className="cta-section section-padding about-cta-section">
<h2 className="cta-h2">Get Your <span>Custom Quote</span> Today</h2>
<p className="cta-sub about-cta-sub" style={{ fontSize: '20px', marginBottom: '32px' }}>
Fill out our job order & fabrication request form or call <strong style={{ color: 'var(--orange)' }}>226-888-7999</strong> to discuss your bulk and custom fabrication needs.
</p>
<div className="cta-btns">
<Link href="/contact" className="btn-primary">Contact Us Now</Link>
<a href="tel:2268887999" className="btn-white-outline">Call Us Today</a>
</div>
</section>
</div>
);
} }

View File

@ -1,3 +1,4 @@
import type { Metadata } from "next";
import Hero from "@/components/Hero"; import Hero from "@/components/Hero";
import TrustBar from "@/components/TrustBar"; import TrustBar from "@/components/TrustBar";
import Products from "@/components/Products"; import Products from "@/components/Products";
@ -7,6 +8,14 @@ import WhoWeServe from "@/components/WhoWeServe";
import StainPromo from "@/components/StainPromo"; import StainPromo from "@/components/StainPromo";
import CTA from "@/components/CTA"; import CTA from "@/components/CTA";
export const metadata: Metadata = {
title: "VG Fence Products | Ontario's B2B Fence Supply Partner",
description: "Premier supplier of professional-grade fencing and railing materials for contractors, builders, and property managers across Ontario.",
alternates: {
canonical: "/"
}
};
export default function Home() { export default function Home() {
return ( return (
<> <>

View File

@ -0,0 +1,118 @@
"use client";
import { useState } from 'react';
import Link from 'next/link';
import Image from 'next/image';
import { products } from '@/data/products';
export default function ProductsPage() {
const [currentPage, setCurrentPage] = useState(1);
const productsPerPage = 12;
// Calculate pagination
const indexOfLastProduct = currentPage * productsPerPage;
const indexOfFirstProduct = indexOfLastProduct - productsPerPage;
const currentProducts = products.slice(indexOfFirstProduct, indexOfLastProduct);
const totalPages = Math.ceil(products.length / productsPerPage);
const paginate = (pageNumber: number) => setCurrentPage(pageNumber);
return (
<div style={{ minHeight: '100vh' }}>
{/* Inner Banner */}
<section className="inner-banner fade-up">
<div className="inner-banner-content">
<h1 className="section-h2">Our <span>Product</span> Catalog</h1>
<div className="banner-breadcrumb" style={{ marginTop: '30px', marginBottom: '0' }}>
<Link href="/">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>
Home
</Link>
<span className="separator">/</span>
<span>Products</span>
</div>
</div>
</section>
{/* Grid Content */}
<section className="products-section section">
<div className="products-grid product-page">
{currentProducts.map((product) => (
<div key={product.slug} className="product-card" style={{ display: 'flex', flexDirection: 'column', height: '100%', textDecoration: 'none' }}>
<div style={{ position: 'relative', width: '100%', height: '240px', borderRadius: '8px', overflow: 'hidden', marginBottom: '20px', background: 'var(--gray-100)' }}>
<Image
src="/assets/about-fencing.png"
alt={product.name}
fill
style={{ objectFit: 'cover' }}
/>
</div>
<div className="product-name">{product.name}</div>
<div className="product-desc" style={{
display: '-webkit-box',
WebkitLineClamp: 3,
WebkitBoxOrient: 'vertical',
overflow: 'hidden',
minHeight: '4.8em',
marginBottom: '16px'
}}>
{product.description}
</div>
<div style={{ marginTop: 'auto' }}>
<div style={{ fontFamily: 'var(--font-display)', fontSize: '18px', fontWeight: 700, color: 'var(--orange)', marginBottom: '16px' }}>
{product.price}
</div>
<Link href={`/products/${product.slug}`} className="btn-primary" style={{ width: '100%', textAlign: 'center', appearance: 'none', display: 'block' }}>
View Product
</Link>
</div>
</div>
))}
</div>
{/* Pagination */}
{totalPages > 1 && (
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', gap: '12px', marginTop: '64px', flexWrap: 'wrap' }}>
<button
onClick={() => setCurrentPage(prev => Math.max(prev - 1, 1))}
disabled={currentPage === 1}
className="btn-secondary"
style={{ padding: '8px 20px', opacity: currentPage === 1 ? 0.3 : 1, color: 'var(--navy)', borderColor: 'var(--navy)' }}
>
Prev
</button>
<div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap', justifyContent: 'center' }}>
{Array.from({ length: totalPages }, (_, i) => i + 1).map((num) => (
<button
key={num}
onClick={() => paginate(num)}
style={{
width: '40px',
height: '40px',
borderRadius: '4px',
border: '1px solid',
borderColor: currentPage === num ? 'var(--orange)' : 'var(--gray-200)',
background: currentPage === num ? 'var(--orange)' : 'transparent',
color: currentPage === num ? 'white' : 'var(--gray-600)',
cursor: 'pointer',
fontWeight: 600
}}
>
{num}
</button>
))}
</div>
<button
onClick={() => setCurrentPage(prev => Math.min(prev + 1, totalPages))}
disabled={currentPage === totalPages}
className="btn-secondary"
style={{ padding: '8px 20px', opacity: currentPage === totalPages ? 0.3 : 1, color: 'var(--navy)', borderColor: 'var(--navy)' }}
>
Next
</button>
</div>
)}
</section>
</div>
);
}

View File

@ -3,6 +3,8 @@ import Link from 'next/link';
import { products } from '@/data/products'; import { products } from '@/data/products';
import { notFound } from 'next/navigation'; import { notFound } from 'next/navigation';
import type { Metadata } from "next";
// Pre-generate all product detail pages at build time // Pre-generate all product detail pages at build time
export function generateStaticParams() { export function generateStaticParams() {
return products.map((product) => ({ return products.map((product) => ({
@ -10,6 +12,21 @@ export function generateStaticParams() {
})); }));
} }
export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }): Promise<Metadata> {
const { slug } = await params;
const product = products.find((p) => p.slug === slug);
if (!product) return { title: "Product Not Found" };
return {
title: `${product.name} | VG Fence Products`,
description: product.description,
alternates: {
canonical: `/products/${product.slug}`
}
};
}
// In Next.js 15+/16, params is a Promise and must be awaited // In Next.js 15+/16, params is a Promise and must be awaited
export default async function ProductDetailPage({ export default async function ProductDetailPage({
params, params,

View File

@ -1,118 +1,14 @@
"use client"; import type { Metadata } from "next";
import { useState } from 'react'; import ProductsClient from "./ProductsClient";
import Link from 'next/link';
import Image from 'next/image'; export const metadata: Metadata = {
import { products } from '@/data/products'; title: "Product Catalog | VG Fence Products",
description: "Explore our extensive range of professional-grade fencing and railing materials. From chain link to ornamental steel and composite panels.",
alternates: {
canonical: "/products"
}
};
export default function ProductsPage() { export default function ProductsPage() {
const [currentPage, setCurrentPage] = useState(1); return <ProductsClient />;
const productsPerPage = 12;
// Calculate pagination
const indexOfLastProduct = currentPage * productsPerPage;
const indexOfFirstProduct = indexOfLastProduct - productsPerPage;
const currentProducts = products.slice(indexOfFirstProduct, indexOfLastProduct);
const totalPages = Math.ceil(products.length / productsPerPage);
const paginate = (pageNumber: number) => setCurrentPage(pageNumber);
return (
<div style={{ minHeight: '100vh' }}>
{/* Inner Banner */}
<section className="inner-banner fade-up">
<div className="inner-banner-content">
<h1 className="section-h2">Our <span>Product</span> Catalog</h1>
<div className="banner-breadcrumb" style={{ marginTop: '30px', marginBottom: '0' }}>
<Link href="/">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>
Home
</Link>
<span className="separator">/</span>
<span>Products</span>
</div>
</div>
</section>
{/* Grid Content */}
<section className="products-section section">
<div className="products-grid product-page">
{currentProducts.map((product) => (
<div key={product.slug} className="product-card" style={{ display: 'flex', flexDirection: 'column', height: '100%', textDecoration: 'none' }}>
<div style={{ position: 'relative', width: '100%', height: '240px', borderRadius: '8px', overflow: 'hidden', marginBottom: '20px', background: 'var(--gray-100)' }}>
<Image
src="/assets/about-fencing.png"
alt={product.name}
fill
style={{ objectFit: 'cover' }}
/>
</div>
<div className="product-name">{product.name}</div>
<div className="product-desc" style={{
display: '-webkit-box',
WebkitLineClamp: 3,
WebkitBoxOrient: 'vertical',
overflow: 'hidden',
minHeight: '4.8em',
marginBottom: '16px'
}}>
{product.description}
</div>
<div style={{ marginTop: 'auto' }}>
<div style={{ fontFamily: 'var(--font-display)', fontSize: '18px', fontWeight: 700, color: 'var(--orange)', marginBottom: '16px' }}>
{product.price}
</div>
<Link href={`/products/${product.slug}`} className="btn-primary" style={{ width: '100%', textAlign: 'center', appearance: 'none', display: 'block' }}>
View Product
</Link>
</div>
</div>
))}
</div>
{/* Pagination */}
{totalPages > 1 && (
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', gap: '12px', marginTop: '64px', flexWrap: 'wrap' }}>
<button
onClick={() => setCurrentPage(prev => Math.max(prev - 1, 1))}
disabled={currentPage === 1}
className="btn-secondary"
style={{ padding: '8px 20px', opacity: currentPage === 1 ? 0.3 : 1, color: 'var(--navy)', borderColor: 'var(--navy)' }}
>
Prev
</button>
<div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap', justifyContent: 'center' }}>
{Array.from({ length: totalPages }, (_, i) => i + 1).map((num) => (
<button
key={num}
onClick={() => paginate(num)}
style={{
width: '40px',
height: '40px',
borderRadius: '4px',
border: '1px solid',
borderColor: currentPage === num ? 'var(--orange)' : 'var(--gray-200)',
background: currentPage === num ? 'var(--orange)' : 'transparent',
color: currentPage === num ? 'white' : 'var(--gray-600)',
cursor: 'pointer',
fontWeight: 600
}}
>
{num}
</button>
))}
</div>
<button
onClick={() => setCurrentPage(prev => Math.min(prev + 1, totalPages))}
disabled={currentPage === totalPages}
className="btn-secondary"
style={{ padding: '8px 20px', opacity: currentPage === totalPages ? 0.3 : 1, color: 'var(--navy)', borderColor: 'var(--navy)' }}
>
Next
</button>
</div>
)}
</section>
</div>
);
} }

View File

@ -0,0 +1,189 @@
import Link from "next/link";
import Image from "next/image";
export const metadata = {
title: "Fence Rentals | VG Fence Products",
description: "Temporary fence rentals for construction sites, events, and security perimeters. Flexible rental terms and quick delivery.",
};
export default function RentalsPage() {
const products = [
{ name: "Temporary Fence Panels", desc: "Durable and easy to assemble", img: "/assets/temp-fence-panel.png" },
{ name: "Temporary Fence Gates", desc: "Single or double swing gates", img: "/assets/about-fencing.png" },
{ name: "Fence Wheels", desc: "Smooth movement for gate panels", img: "/assets/manufacturing-hero.png" },
{ name: "Wind Bracing", desc: "Prevent fence panels from tipping", img: "/assets/about-fencing.png" },
{ name: "Fence Bases", desc: "Concrete, rubber, or plastic bases for stability", img: "/assets/manufacturing-hero.png" },
{ name: "Fence Clamps & Accessories", desc: "Secure connections for panels", img: "/assets/about-fencing.png" },
{ name: "Sandbags / Ballast", desc: "Extra stability for windy or busy areas", img: "/assets/manufacturing-hero.png" },
{ name: "Debris & Safety Netting", desc: "Optional add-ons for construction sites", img: "/assets/about-fencing.png" },
];
const applications = [
{ id: 1, title: "Construction Sites", desc: "Keep workers and equipment secure" },
{ id: 2, title: "Events and Festivals", desc: "Crowd control and perimeter fencing" },
{ id: 3, title: "Sports and Film Production", desc: "Temporary barriers for staff and public safety" },
{ id: 4, title: "Short-Term and Long-Term Projects", desc: "Flexible rental periods" },
];
return (
<div className="rentals-page">
{/* ── INNER BANNER ── */}
<section className="inner-banner fade-up">
<div className="inner-banner-content">
<h1 className="section-h2">Fence <span>Rentals</span></h1>
<div className="banner-breadcrumb" style={{ marginTop: '30px', marginBottom: '0' }}>
<Link href="/">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>
Home
</Link>
<span className="separator">/</span>
<span>Rentals</span>
</div>
</div>
</section>
{/* ── INTRO SECTION (REDESIGNED) ── */}
<section className="section rental-intro">
<div className="container">
<div className="about-story-layout" style={{ alignItems: 'center' }}>
<div className="about-story-text" style={{ textAlign: 'left' }}>
<div className="section-eyebrow">FENCE RENTALS</div>
<h2 className="section-h2" style={{ fontSize: '48px', lineHeight: '1.1', marginBottom: '30px' }}>
Temporary Fence Rentals for <span>Construction, Events, and Security</span>
</h2>
<p style={{ fontSize: '16px', color: 'var(--gray-600)', lineHeight: '1.8', marginBottom: '24px' }}>
VG Fence provides high-quality temporary fencing solutions for contractors, event organizers, and property managers. Our rentals are flexible, reliable, and designed to secure construction sites, public events, and temporary perimeters.
</p>
<div style={{ display: 'flex', alignItems: 'center', gap: '15px', padding: '15px 20px', background: 'rgba(232,87,42,0.05)', borderLeft: '4px solid var(--orange)', borderRadius: '4px', marginBottom: '32px' }}>
{/* <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="var(--orange)" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg> */}
<span style={{ fontSize: '15px', fontWeight: 700, color: 'var(--navy)' }}>Installation support for temporary fences is available for select projects</span>
</div>
{/* <div className="progress-container">
<div className="circular-progress">
<span className="progress-value">85%</span>
</div>
<div className="progress-info">
<h4>Satisfied Customer</h4>
<p>Reliable support for contractors and event managers across Ontario projects.</p>
</div>
</div> */}
</div>
<div className="about-image-column">
<div className="about-image-wrapper" style={{ padding: '15px', background: 'var(--white)', boxShadow: '0 30px 60px rgba(0,0,0,0.1)', borderRadius: '4px' }}>
<Image
src="/assets/about-fencing.png"
alt="Industrial fencing installation experts"
fill
style={{ objectFit: 'cover', borderRadius: '2px' }}
/>
</div>
</div>
</div>
</div>
</section>
{/* ── PRODUCTS SECTION ── */}
<section className="section rental-products" style={{ background: 'var(--gray-100)' }}>
<div className="container">
<div style={{ textAlign: 'center', marginBottom: '60px' }}>
<div className="section-eyebrow" style={{ justifyContent: 'center' }}>Equipment List</div>
<h2 className="section-h2">Products & Equipment <span>Available</span></h2>
</div>
<div className="rental-grid">
{products.map((p, i) => (
<div key={i} className="rental-item-card">
<div className="rental-item-img">
<Image src={p.img} alt={p.name} fill style={{ objectFit: 'cover' }} />
</div>
<div className="rental-item-body">
<h3 className="rental-item-name">{p.name}</h3>
<p className="rental-item-desc">{p.desc}</p>
</div>
</div>
))}
</div>
</div>
</section>
{/* ── APPLICATIONS SECTION ── */}
<section className="section rental-apps">
<div className="container">
<div style={{ textAlign: 'center', marginBottom: '60px' }}>
<div className="section-eyebrow" style={{ justifyContent: 'center' }}>Tailored Solutions</div>
<h2 className="section-h2">Applications</h2>
</div>
<div className="app-grid">
{applications.map((app) => (
<div key={app.id} className="app-card">
<div className="app-num">{app.id}</div>
<div className="app-content">
<h3 className="app-title">{app.title}</h3>
<p className="app-desc">{app.desc}</p>
</div>
</div>
))}
</div>
</div>
</section>
{/* ── WHY CHOOSE SECTION (IMAGE + TIMELINE) ── */}
<section className="rental-why-section fade-up">
<div className="rental-why-img">
<Image
src="/assets/fence-rentals-hero.png"
alt="VG Fence Temporary Fencing"
fill
style={{ objectFit: 'cover' }}
/>
</div>
<div className="rental-why-content">
<h2 className="rental-why-title">Why Choose VG Fence Rentals</h2>
<ul className="rental-timeline">
<li className="rental-timeline-item">
<div className="timeline-icon">
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M12 2v20"/><path d="m5 15 7-7 7 7"/></svg>
</div>
<div className="timeline-text">Flexible rental terms (daily, weekly, monthly)</div>
</li>
<li className="rental-timeline-item">
<div className="timeline-icon">
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M14 9V5a3 3 0 0 0-3-3l-4 9v11h11.28a2 2 0 0 0 2-1.7L22 14v-2a2 2 0 0 0-2-2h-6Z"/><path d="M7 22H4a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h3"/></svg>
</div>
<div className="timeline-text">Durable and professional-grade materials</div>
</li>
<li className="rental-timeline-item">
<div className="timeline-icon">
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="1" y="3" width="15" height="13"/><polyline points="16 8 20 8 23 11 23 16 16 16 16 8"/><circle cx="5.5" cy="18.5" r="2.5"/><circle cx="18.5" cy="18.5" r="2.5"/></svg>
</div>
<div className="timeline-text">Quick delivery & setup support available</div>
</li>
<li className="rental-timeline-item">
<div className="timeline-icon">
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>
</div>
<div className="timeline-text">Galvanized or black finishes</div>
</li>
<li className="rental-timeline-item">
<div className="timeline-icon">
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>
</div>
<div className="timeline-text">Reliable support for contractors and event managers</div>
</li>
</ul>
<div style={{ marginTop: '60px', borderTop: '1px solid rgba(255,255,255,0.1)', paddingTop: '40px' }}>
<h2 className="section-h2" style={{ color: 'var(--white)', fontSize: '32px', marginBottom: '30px' }}>Secure Your <span>Temporary Fencing</span> Today</h2>
<Link href="/contact" className="btn-primary">Request a Quote</Link>
</div>
</div>
</section>
</div>
);
}

View File

@ -1,189 +1,14 @@
import Link from "next/link"; import type { Metadata } from "next";
import Image from "next/image"; import RentalsClient from "./RentalsClient";
export const metadata = { export const metadata: Metadata = {
title: "Fence Rentals | VG Fence Products", title: "Temporary Fence Rentals | VG Fence Products",
description: "Temporary fence rentals for construction sites, events, and security perimeters. Flexible rental terms and quick delivery.", description: "Reliable temporary fencing solutions for construction sites and public events in Ontario. Daily, weekly, and monthly rental terms available.",
alternates: {
canonical: "/rentals"
}
}; };
export default function RentalsPage() { export default function RentalsPage() {
const products = [ return <RentalsClient />;
{ name: "Temporary Fence Panels", desc: "Durable and easy to assemble", img: "/assets/temp-fence-panel.png" },
{ name: "Temporary Fence Gates", desc: "Single or double swing gates", img: "/assets/about-fencing.png" },
{ name: "Fence Wheels", desc: "Smooth movement for gate panels", img: "/assets/manufacturing-hero.png" },
{ name: "Wind Bracing", desc: "Prevent fence panels from tipping", img: "/assets/about-fencing.png" },
{ name: "Fence Bases", desc: "Concrete, rubber, or plastic bases for stability", img: "/assets/manufacturing-hero.png" },
{ name: "Fence Clamps & Accessories", desc: "Secure connections for panels", img: "/assets/about-fencing.png" },
{ name: "Sandbags / Ballast", desc: "Extra stability for windy or busy areas", img: "/assets/manufacturing-hero.png" },
{ name: "Debris & Safety Netting", desc: "Optional add-ons for construction sites", img: "/assets/about-fencing.png" },
];
const applications = [
{ id: 1, title: "Construction Sites", desc: "Keep workers and equipment secure" },
{ id: 2, title: "Events and Festivals", desc: "Crowd control and perimeter fencing" },
{ id: 3, title: "Sports and Film Production", desc: "Temporary barriers for staff and public safety" },
{ id: 4, title: "Short-Term and Long-Term Projects", desc: "Flexible rental periods" },
];
return (
<div className="rentals-page">
{/* ── INNER BANNER ── */}
<section className="inner-banner fade-up">
<div className="inner-banner-content">
<h1 className="section-h2">Fence <span>Rentals</span></h1>
<div className="banner-breadcrumb" style={{ marginTop: '30px', marginBottom: '0' }}>
<Link href="/">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>
Home
</Link>
<span className="separator">/</span>
<span>Rentals</span>
</div>
</div>
</section>
{/* ── INTRO SECTION (REDESIGNED) ── */}
<section className="section rental-intro">
<div className="container">
<div className="about-story-layout" style={{ alignItems: 'center' }}>
<div className="about-story-text" style={{ textAlign: 'left' }}>
<div className="section-eyebrow">FENCE RENTALS</div>
<h2 className="section-h2" style={{ fontSize: '48px', lineHeight: '1.1', marginBottom: '30px' }}>
Temporary Fence Rentals for <span>Construction, Events, and Security</span>
</h2>
<p style={{ fontSize: '16px', color: 'var(--gray-600)', lineHeight: '1.8', marginBottom: '24px' }}>
VG Fence provides high-quality temporary fencing solutions for contractors, event organizers, and property managers. Our rentals are flexible, reliable, and designed to secure construction sites, public events, and temporary perimeters.
</p>
<div style={{ display: 'flex', alignItems: 'center', gap: '15px', padding: '15px 20px', background: 'rgba(232,87,42,0.05)', borderLeft: '4px solid var(--orange)', borderRadius: '4px', marginBottom: '32px' }}>
{/* <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="var(--orange)" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg> */}
<span style={{ fontSize: '15px', fontWeight: 700, color: 'var(--navy)' }}>Installation support for temporary fences is available for select projects</span>
</div>
{/* <div className="progress-container">
<div className="circular-progress">
<span className="progress-value">85%</span>
</div>
<div className="progress-info">
<h4>Satisfied Customer</h4>
<p>Reliable support for contractors and event managers across Ontario projects.</p>
</div>
</div> */}
</div>
<div className="about-image-column">
<div className="about-image-wrapper" style={{ padding: '15px', background: 'var(--white)', boxShadow: '0 30px 60px rgba(0,0,0,0.1)', borderRadius: '4px' }}>
<Image
src="/assets/about-fencing.png"
alt="Industrial fencing installation experts"
fill
style={{ objectFit: 'cover', borderRadius: '2px' }}
/>
</div>
</div>
</div>
</div>
</section>
{/* ── PRODUCTS SECTION ── */}
<section className="section rental-products" style={{ background: 'var(--gray-100)' }}>
<div className="container">
<div style={{ textAlign: 'center', marginBottom: '60px' }}>
<div className="section-eyebrow" style={{ justifyContent: 'center' }}>Equipment List</div>
<h2 className="section-h2">Products & Equipment <span>Available</span></h2>
</div>
<div className="rental-grid">
{products.map((p, i) => (
<div key={i} className="rental-item-card">
<div className="rental-item-img">
<Image src={p.img} alt={p.name} fill style={{ objectFit: 'cover' }} />
</div>
<div className="rental-item-body">
<h3 className="rental-item-name">{p.name}</h3>
<p className="rental-item-desc">{p.desc}</p>
</div>
</div>
))}
</div>
</div>
</section>
{/* ── APPLICATIONS SECTION ── */}
<section className="section rental-apps">
<div className="container">
<div style={{ textAlign: 'center', marginBottom: '60px' }}>
<div className="section-eyebrow" style={{ justifyContent: 'center' }}>Tailored Solutions</div>
<h2 className="section-h2">Applications</h2>
</div>
<div className="app-grid">
{applications.map((app) => (
<div key={app.id} className="app-card">
<div className="app-num">{app.id}</div>
<div className="app-content">
<h3 className="app-title">{app.title}</h3>
<p className="app-desc">{app.desc}</p>
</div>
</div>
))}
</div>
</div>
</section>
{/* ── WHY CHOOSE SECTION (IMAGE + TIMELINE) ── */}
<section className="rental-why-section fade-up">
<div className="rental-why-img">
<Image
src="/assets/fence-rentals-hero.png"
alt="VG Fence Temporary Fencing"
fill
style={{ objectFit: 'cover' }}
/>
</div>
<div className="rental-why-content">
<h2 className="rental-why-title">Why Choose VG Fence Rentals</h2>
<ul className="rental-timeline">
<li className="rental-timeline-item">
<div className="timeline-icon">
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M12 2v20"/><path d="m5 15 7-7 7 7"/></svg>
</div>
<div className="timeline-text">Flexible rental terms (daily, weekly, monthly)</div>
</li>
<li className="rental-timeline-item">
<div className="timeline-icon">
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M14 9V5a3 3 0 0 0-3-3l-4 9v11h11.28a2 2 0 0 0 2-1.7L22 14v-2a2 2 0 0 0-2-2h-6Z"/><path d="M7 22H4a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h3"/></svg>
</div>
<div className="timeline-text">Durable and professional-grade materials</div>
</li>
<li className="rental-timeline-item">
<div className="timeline-icon">
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="1" y="3" width="15" height="13"/><polyline points="16 8 20 8 23 11 23 16 16 16 16 8"/><circle cx="5.5" cy="18.5" r="2.5"/><circle cx="18.5" cy="18.5" r="2.5"/></svg>
</div>
<div className="timeline-text">Quick delivery & setup support available</div>
</li>
<li className="rental-timeline-item">
<div className="timeline-icon">
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>
</div>
<div className="timeline-text">Galvanized or black finishes</div>
</li>
<li className="rental-timeline-item">
<div className="timeline-icon">
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>
</div>
<div className="timeline-text">Reliable support for contractors and event managers</div>
</li>
</ul>
<div style={{ marginTop: '60px', borderTop: '1px solid rgba(255,255,255,0.1)', paddingTop: '40px' }}>
<h2 className="section-h2" style={{ color: 'var(--white)', fontSize: '32px', marginBottom: '30px' }}>Secure Your <span>Temporary Fencing</span> Today</h2>
<Link href="/contact" className="btn-primary">Request a Quote</Link>
</div>
</div>
</section>
</div>
);
} }

View File

@ -1,7 +1,105 @@
"use client"; "use client";
import React, { useState, useEffect } from "react";
import ReCAPTCHA from "react-google-recaptcha";
import axios from "axios";
import Link from 'next/link'; import Link from 'next/link';
export default function Hero() { export default function Hero() {
const [formData, setFormData] = useState({
company: "",
name: "",
phone: "",
email: "",
product: "",
city: "",
quantity: ""
});
const [formErrors, setFormErrors] = useState<any>({});
const [captchaToken, setCaptchaToken] = useState<string | null>(null);
const [alert, setAlert] = useState({ show: false, type: "", message: "" });
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
const { name, value } = e.target;
setFormData((prev) => ({ ...prev, [name]: value }));
};
const handleCaptchaChange = (token: string | null) => {
setCaptchaToken(token);
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
const errors: any = {};
if (!formData.name.trim()) errors.name = "Name is required.";
if (!formData.phone.trim()) errors.phone = "Phone is required.";
if (!formData.email.trim()) errors.email = "Email is required.";
if (!formData.product.trim()) errors.product = "Please select a product.";
if (!captchaToken) errors.captcha = "Please verify the CAPTCHA.";
setFormErrors(errors);
if (Object.keys(errors).length > 0) return;
const emailData = {
name: formData.name,
email: formData.email,
phone: formData.phone,
message: `
<b>Company:</b> ${formData.company}<br />
<b>Product:</b> ${formData.product}<br />
<b>Job Site City:</b> ${formData.city}<br />
<b>Quantity:</b> ${formData.quantity}
`,
to: "info@vgfenceproducts.com",
senderName: "VG Fence Hero Form",
recaptchaToken: captchaToken,
};
setAlert({ show: true, type: "info", message: "Sending your request..." });
try {
await axios.post("https://mailserver.metatronnest.com/send", emailData, {
headers: { "Content-Type": "application/json" },
});
setAlert({
show: true,
type: "success",
message: "Thank you! Your quote request has been sent successfully.",
});
setFormData({
company: "",
name: "",
phone: "",
email: "",
product: "",
city: "",
quantity: ""
});
setCaptchaToken(null);
setFormErrors({});
} catch (error) {
console.error("❌ Error sending email:", error);
setAlert({
show: true,
type: "danger",
message: "Failed to send request. Please try again later.",
});
}
};
useEffect(() => {
if (alert.show && alert.type !== "info") {
const timer = setTimeout(() => {
setAlert((prev) => ({ ...prev, show: false }));
}, 5000);
return () => clearTimeout(timer);
}
}, [alert.show, alert.type]);
return ( return (
<section className="hero"> <section className="hero">
<div className="hero-pattern"></div> <div className="hero-pattern"></div>
@ -48,30 +146,82 @@ export default function Hero() {
<div className="quote-card"> <div className="quote-card">
<div className="quote-card-title">Request a quote</div> <div className="quote-card-title">Request a quote</div>
<div className="quote-card-sub">Response within 2 business hours · Contractor pricing available</div> <div className="quote-card-sub">Response within 2 business hours · Contractor pricing available</div>
<form className="quote-form">
{alert.show && (
<div className={`alert alert-${alert.type === 'danger' ? 'danger' : (alert.type === 'info' ? 'info' : 'success')} mb-4`} style={{
padding: '12px 16px',
borderRadius: '8px',
fontSize: '14px',
marginBottom: '20px',
background: alert.type === 'danger' ? '#fee2e2' : (alert.type === 'info' ? '#e0f2fe' : '#f0fdf4'),
color: alert.type === 'danger' ? '#991b1b' : (alert.type === 'info' ? '#075985' : '#166534'),
border: `1px solid ${alert.type === 'danger' ? '#fecaca' : (alert.type === 'info' ? '#bae6fd' : '#bbf7d0')}`
}}>
{alert.message}
</div>
)}
<form className="quote-form" onSubmit={handleSubmit}>
<div className="form-row"> <div className="form-row">
<div className="form-group"> <div className="form-group">
<label className="form-label">Company name</label> <label className="form-label">Company name</label>
<input className="form-input" type="text" placeholder="ABC Fence Co." /> <input
className="form-input"
type="text"
name="company"
placeholder="ABC Fence Co."
value={formData.company}
onChange={handleChange}
/>
</div> </div>
<div className="form-group"> <div className="form-group">
<label className="form-label">Your name</label> <label className="form-label">Your name</label>
<input className="form-input" type="text" placeholder="John Smith" /> <input
className="form-input"
type="text"
name="name"
placeholder="John Smith"
value={formData.name}
onChange={handleChange}
style={{ borderColor: formErrors.name ? '#ef4444' : '' }}
/>
</div> </div>
</div> </div>
<div className="form-row"> <div className="form-row">
<div className="form-group"> <div className="form-group">
<label className="form-label">Phone</label> <label className="form-label">Phone</label>
<input className="form-input" type="tel" placeholder="519-xxx-xxxx" /> <input
className="form-input"
type="tel"
name="phone"
placeholder="519-xxx-xxxx"
value={formData.phone}
onChange={handleChange}
style={{ borderColor: formErrors.phone ? '#ef4444' : '' }}
/>
</div> </div>
<div className="form-group"> <div className="form-group">
<label className="form-label">Email</label> <label className="form-label">Email</label>
<input className="form-input" type="email" placeholder="you@company.com" /> <input
className="form-input"
type="email"
name="email"
placeholder="you@company.com"
value={formData.email}
onChange={handleChange}
style={{ borderColor: formErrors.email ? '#ef4444' : '' }}
/>
</div> </div>
</div> </div>
<div className="form-group"> <div className="form-group">
<label className="form-label">Product needed</label> <label className="form-label">Product needed</label>
<select className="form-select"> <select
className="form-select"
name="product"
value={formData.product}
onChange={handleChange}
style={{ borderColor: formErrors.product ? '#ef4444' : '' }}
>
<option value="">Select a product...</option> <option value="">Select a product...</option>
<option>Aluminum railing</option> <option>Aluminum railing</option>
<option>Chain link fence commercial</option> <option>Chain link fence commercial</option>
@ -88,14 +238,39 @@ export default function Hero() {
<div className="form-row"> <div className="form-row">
<div className="form-group"> <div className="form-group">
<label className="form-label">Job site city</label> <label className="form-label">Job site city</label>
<input className="form-input" type="text" placeholder="Kitchener, Guelph..." /> <input
className="form-input"
type="text"
name="city"
placeholder="Kitchener, Guelph..."
value={formData.city}
onChange={handleChange}
/>
</div> </div>
<div className="form-group"> <div className="form-group">
<label className="form-label">Approximate quantity</label> <label className="form-label">Approximate quantity</label>
<input className="form-input" type="text" placeholder="e.g. 200 linear ft" /> <input
className="form-input"
type="text"
name="quantity"
placeholder="e.g. 200 linear ft"
value={formData.quantity}
onChange={handleChange}
/>
</div> </div>
</div> </div>
<button type="submit" className="form-submit" onClick={(e) => e.preventDefault()}>Send quote request </button>
<div className="form-group">
<ReCAPTCHA
sitekey="6LekfpwrAAAAAOTwuP1d2gg-Fv9UEsAjE2gjOQJl"
onChange={handleCaptchaChange}
/>
{formErrors.captcha && <small style={{ color: '#ef4444', fontSize: '11px', marginTop: '4px', display: 'block' }}>{formErrors.captcha}</small>}
</div>
<button type="submit" className="form-submit" disabled={alert.type === "info"}>
{alert.type === "info" ? "Sending..." : "Send quote request →"}
</button>
<div className="form-note">Or call us directly · info@vgfenceproducts.com</div> <div className="form-note">Or call us directly · info@vgfenceproducts.com</div>
</form> </form>
</div> </div>

203
package-lock.json generated
View File

@ -8,14 +8,19 @@
"name": "vgfence-website", "name": "vgfence-website",
"version": "0.1.0", "version": "0.1.0",
"dependencies": { "dependencies": {
"axios": "^1.15.0",
"next": "16.2.4", "next": "16.2.4",
"react": "19.2.4", "react": "19.2.4",
"react-dom": "19.2.4" "react-dom": "19.2.4",
"react-google-recaptcha": "^3.1.0",
"react-icons": "^5.6.0",
"swiper": "^12.1.3"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^20", "@types/node": "^20",
"@types/react": "^19", "@types/react": "^19",
"@types/react-dom": "^19", "@types/react-dom": "^19",
"@types/react-google-recaptcha": "^2.1.9",
"eslint": "^9", "eslint": "^9",
"eslint-config-next": "16.2.4", "eslint-config-next": "16.2.4",
"typescript": "^5" "typescript": "^5"
@ -1289,6 +1294,16 @@
"@types/react": "^19.2.0" "@types/react": "^19.2.0"
} }
}, },
"node_modules/@types/react-google-recaptcha": {
"version": "2.1.9",
"resolved": "https://registry.npmjs.org/@types/react-google-recaptcha/-/react-google-recaptcha-2.1.9.tgz",
"integrity": "sha512-nT31LrBDuoSZJN4QuwtQSF3O89FVHC4jLhM+NtKEmVF5R1e8OY0Jo4//x2Yapn2aNHguwgX5doAq8Zo+Ehd0ug==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/react": "*"
}
},
"node_modules/@typescript-eslint/eslint-plugin": { "node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.58.2", "version": "8.58.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.58.2.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.58.2.tgz",
@ -2103,6 +2118,12 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"license": "MIT"
},
"node_modules/available-typed-arrays": { "node_modules/available-typed-arrays": {
"version": "1.0.7", "version": "1.0.7",
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
@ -2129,6 +2150,17 @@
"node": ">=4" "node": ">=4"
} }
}, },
"node_modules/axios": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.15.0.tgz",
"integrity": "sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.11",
"form-data": "^4.0.5",
"proxy-from-env": "^2.1.0"
}
},
"node_modules/axobject-query": { "node_modules/axobject-query": {
"version": "4.1.0", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
@ -2239,7 +2271,6 @@
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"es-errors": "^1.3.0", "es-errors": "^1.3.0",
@ -2339,6 +2370,18 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"license": "MIT",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/concat-map": { "node_modules/concat-map": {
"version": "0.0.1", "version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@ -2497,6 +2540,15 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"license": "MIT",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/detect-libc": { "node_modules/detect-libc": {
"version": "2.1.2", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
@ -2524,7 +2576,6 @@
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"call-bind-apply-helpers": "^1.0.1", "call-bind-apply-helpers": "^1.0.1",
@ -2622,7 +2673,6 @@
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
@ -2632,7 +2682,6 @@
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
@ -2670,7 +2719,6 @@
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"es-errors": "^1.3.0" "es-errors": "^1.3.0"
@ -2683,7 +2731,6 @@
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"es-errors": "^1.3.0", "es-errors": "^1.3.0",
@ -3280,6 +3327,26 @@
"dev": true, "dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/follow-redirects": {
"version": "1.16.0",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz",
"integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"license": "MIT",
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/for-each": { "node_modules/for-each": {
"version": "0.3.5", "version": "0.3.5",
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
@ -3296,11 +3363,26 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/form-data": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
"integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/function-bind": { "node_modules/function-bind": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"dev": true,
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
@ -3361,7 +3443,6 @@
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"call-bind-apply-helpers": "^1.0.2", "call-bind-apply-helpers": "^1.0.2",
@ -3386,7 +3467,6 @@
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"dunder-proto": "^1.0.1", "dunder-proto": "^1.0.1",
@ -3474,7 +3554,6 @@
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
@ -3539,7 +3618,6 @@
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
@ -3552,7 +3630,6 @@
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"has-symbols": "^1.0.3" "has-symbols": "^1.0.3"
@ -3568,7 +3645,6 @@
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"function-bind": "^1.1.2" "function-bind": "^1.1.2"
@ -3594,6 +3670,15 @@
"hermes-estree": "0.25.1" "hermes-estree": "0.25.1"
} }
}, },
"node_modules/hoist-non-react-statics": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
"integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
"license": "BSD-3-Clause",
"dependencies": {
"react-is": "^16.7.0"
}
},
"node_modules/ignore": { "node_modules/ignore": {
"version": "5.3.2", "version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
@ -4097,7 +4182,6 @@
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/js-yaml": { "node_modules/js-yaml": {
@ -4247,7 +4331,6 @@
"version": "1.4.0", "version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"js-tokens": "^3.0.0 || ^4.0.0" "js-tokens": "^3.0.0 || ^4.0.0"
@ -4270,7 +4353,6 @@
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
@ -4300,6 +4382,27 @@
"node": ">=8.6" "node": ">=8.6"
} }
}, },
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/minimatch": { "node_modules/minimatch": {
"version": "3.1.5", "version": "3.1.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
@ -4454,7 +4557,6 @@
"version": "4.1.1", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
@ -4752,7 +4854,6 @@
"version": "15.8.1", "version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"loose-envify": "^1.4.0", "loose-envify": "^1.4.0",
@ -4760,6 +4861,15 @@
"react-is": "^16.13.1" "react-is": "^16.13.1"
} }
}, },
"node_modules/proxy-from-env": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
"integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==",
"license": "MIT",
"engines": {
"node": ">=10"
}
},
"node_modules/punycode": { "node_modules/punycode": {
"version": "2.3.1", "version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@ -4800,6 +4910,19 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/react-async-script": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/react-async-script/-/react-async-script-1.2.0.tgz",
"integrity": "sha512-bCpkbm9JiAuMGhkqoAiC0lLkb40DJ0HOEJIku+9JDjxX3Rcs+ztEOG13wbrOskt3n2DTrjshhaQ/iay+SnGg5Q==",
"license": "MIT",
"dependencies": {
"hoist-non-react-statics": "^3.3.0",
"prop-types": "^15.5.0"
},
"peerDependencies": {
"react": ">=16.4.1"
}
},
"node_modules/react-dom": { "node_modules/react-dom": {
"version": "19.2.4", "version": "19.2.4",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz",
@ -4812,11 +4935,32 @@
"react": "^19.2.4" "react": "^19.2.4"
} }
}, },
"node_modules/react-google-recaptcha": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/react-google-recaptcha/-/react-google-recaptcha-3.1.0.tgz",
"integrity": "sha512-cYW2/DWas8nEKZGD7SCu9BSuVz8iOcOLHChHyi7upUuVhkpkhYG/6N3KDiTQ3XAiZ2UAZkfvYKMfAHOzBOcGEg==",
"license": "MIT",
"dependencies": {
"prop-types": "^15.5.0",
"react-async-script": "^1.2.0"
},
"peerDependencies": {
"react": ">=16.4.1"
}
},
"node_modules/react-icons": {
"version": "5.6.0",
"resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.6.0.tgz",
"integrity": "sha512-RH93p5ki6LfOiIt0UtDyNg/cee+HLVR6cHHtW3wALfo+eOHTp8RnU2kRkI6E+H19zMIs03DyxUG/GfZMOGvmiA==",
"license": "MIT",
"peerDependencies": {
"react": "*"
}
},
"node_modules/react-is": { "node_modules/react-is": {
"version": "16.13.1", "version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/reflect.getprototypeof": { "node_modules/reflect.getprototypeof": {
@ -5434,6 +5578,25 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/swiper": {
"version": "12.1.3",
"resolved": "https://registry.npmjs.org/swiper/-/swiper-12.1.3.tgz",
"integrity": "sha512-XcWlVmkHFICI4fuoJKgbp8PscDcS4i7pBH8nwJRBi3dpQvhCySwsWRYm4bOf/BzKVWkHOYaFw7qz9uBSrY3oug==",
"funding": [
{
"type": "patreon",
"url": "https://www.patreon.com/swiperjs"
},
{
"type": "open_collective",
"url": "http://opencollective.com/swiper"
}
],
"license": "MIT",
"engines": {
"node": ">= 4.7.0"
}
},
"node_modules/tinyglobby": { "node_modules/tinyglobby": {
"version": "0.2.16", "version": "0.2.16",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz",

View File

@ -9,14 +9,19 @@
"lint": "eslint" "lint": "eslint"
}, },
"dependencies": { "dependencies": {
"axios": "^1.15.0",
"next": "16.2.4", "next": "16.2.4",
"react": "19.2.4", "react": "19.2.4",
"react-dom": "19.2.4" "react-dom": "19.2.4",
"react-google-recaptcha": "^3.1.0",
"react-icons": "^5.6.0",
"swiper": "^12.1.3"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^20", "@types/node": "^20",
"@types/react": "^19", "@types/react": "^19",
"@types/react-dom": "^19", "@types/react-dom": "^19",
"@types/react-google-recaptcha": "^2.1.9",
"eslint": "^9", "eslint": "^9",
"eslint-config-next": "16.2.4", "eslint-config-next": "16.2.4",
"typescript": "^5" "typescript": "^5"