Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 68e5f92dde | |||
| b76d1546b5 | |||
| 2eb4be8f1c | |||
| 99daeff82c | |||
| c0c6b67710 | |||
| a6f99ff072 | |||
| b842d2ba67 | |||
| 05a3691716 | |||
| 498aafdfb6 | |||
| b67d96e954 | |||
| 5e6075de8c | |||
| 3d9c52f1c4 |
121
src/app/page.tsx
121
src/app/page.tsx
@@ -1,8 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { ThemeProvider } from "@/providers/themeProvider/ThemeProvider";
|
||||
import ReactLenis, { useLenis } from "lenis/react";
|
||||
import { useState } from 'react';
|
||||
import ContactCTA from '@/components/sections/contact/ContactCTA';
|
||||
import FaqDouble from '@/components/sections/faq/FaqDouble';
|
||||
import FeatureCardTwentyThree from '@/components/sections/feature/FeatureCardTwentyThree';
|
||||
@@ -11,38 +9,9 @@ import HeroLogoBillboardSplit from '@/components/sections/hero/HeroLogoBillboard
|
||||
import MetricCardTwo from '@/components/sections/metrics/MetricCardTwo';
|
||||
import NavbarLayoutFloatingInline from '@/components/navbar/NavbarLayoutFloatingInline';
|
||||
import TestimonialCardTwo from '@/components/sections/testimonial/TestimonialCardTwo';
|
||||
import Input from '@/components/form/Input';
|
||||
import { MessageCircle, Sparkles, Star, Wrench } from "lucide-react";
|
||||
|
||||
export default function LandingPage() {
|
||||
const lenis = useLenis();
|
||||
|
||||
const scrollToSection = (sectionId) => {
|
||||
const cleanId = String(sectionId).replace(/^#/, "");
|
||||
if (lenis) {
|
||||
lenis.scrollTo(`#${cleanId}`, { offset: 0 });
|
||||
} else {
|
||||
const element = document.getElementById(cleanId);
|
||||
if (!element) return;
|
||||
element.scrollIntoView({ behavior: "smooth", block: "start" });
|
||||
}
|
||||
};
|
||||
|
||||
const [name, setName] = useState("");
|
||||
const [email, setEmail] = useState("");
|
||||
const [phone, setPhone] = useState("");
|
||||
const [message, setMessage] = useState("");
|
||||
|
||||
const handleSubmitQuote = (e) => {
|
||||
e.preventDefault();
|
||||
console.log({ name, email, phone, message });
|
||||
alert("Quote request submitted! We'll be in touch soon.");
|
||||
setName("");
|
||||
setEmail("");
|
||||
setPhone("");
|
||||
setMessage("");
|
||||
};
|
||||
|
||||
return (
|
||||
<ThemeProvider
|
||||
defaultButtonVariant="icon-arrow"
|
||||
@@ -56,7 +25,7 @@ export default function LandingPage() {
|
||||
secondaryButtonStyle="layered"
|
||||
headingFontWeight="bold"
|
||||
>
|
||||
<ReactLenis root>
|
||||
|
||||
<div id="nav" data-section="nav">
|
||||
<NavbarLayoutFloatingInline
|
||||
navItems={[
|
||||
@@ -71,7 +40,7 @@ export default function LandingPage() {
|
||||
]}
|
||||
brandName="Results Roofing"
|
||||
button={{
|
||||
text: "Free Estimate", onClick: () => scrollToSection("quote")}}
|
||||
text: "Free Estimate", href: "#quote"}}
|
||||
animateOnLoad={true}
|
||||
/>
|
||||
</div>
|
||||
@@ -84,9 +53,9 @@ export default function LandingPage() {
|
||||
description="Roofing that feels premium before the first shingle goes on. Results Roofing helps Dallas homeowners with inspections, repairs, replacements, and insurance guidance through a polished, high-trust experience from start to finish."
|
||||
buttons={[
|
||||
{
|
||||
text: "Get My Free Roof Inspection", onClick: () => scrollToSection("quote")},
|
||||
text: "Get My Free Roof Inspection", href: "#quote"},
|
||||
{
|
||||
text: "See Why Dallas Trusts Us", onClick: () => scrollToSection("reviews")},
|
||||
text: "See Why Dallas Trusts Us", href: "#reviews"},
|
||||
]}
|
||||
layoutOrder="default"
|
||||
imageSrc="http://img.b2bpic.net/free-photo/chisinau-arena-sunset-moldova_1268-16015.jpg"
|
||||
@@ -209,11 +178,11 @@ export default function LandingPage() {
|
||||
background={{
|
||||
variant: "radial-gradient"}}
|
||||
tag="Ready when you are"
|
||||
title="Protect your home with a roof that looks better and performs better."
|
||||
description="Whether you need an inspection, a fast repair, or a full replacement, Results Roofing is positioned here as the premium choice that still feels approachable."
|
||||
title="Get Your Free Roofing Estimate"
|
||||
description="Need an inspection, repair, or full roof replacement? Tell us what is going on and our team will reach out to schedule your free estimate."
|
||||
buttons={[
|
||||
{
|
||||
text: "Get a Free Estimate", onClick: () => scrollToSection("quote")},
|
||||
text: "Request Free Estimate", href: "#quote"},
|
||||
{
|
||||
text: "Call Results Roofing", href: "tel:+12145550199"},
|
||||
]}
|
||||
@@ -223,68 +192,6 @@ export default function LandingPage() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div id="quote" data-section="quote" className="py-20 bg-card rounded-lg mx-auto max-w-content-width px-4 md:px-8">
|
||||
<div className="text-center mb-12">
|
||||
<h2 className="text-4xl md:text-5xl font-bold leading-tight text-foreground">Get Your Free Estimate</h2>
|
||||
<p className="mt-4 text-lg text-foreground/80">Fill out the form below and we'll get back to you shortly.</p>
|
||||
</div>
|
||||
<form onSubmit={handleSubmitQuote} className="max-w-xl mx-auto space-y-6">
|
||||
<div>
|
||||
<label htmlFor="name" className="block text-sm font-medium text-foreground">Name</label>
|
||||
<Input
|
||||
id="name"
|
||||
type="text"
|
||||
placeholder="Your Name"
|
||||
value={name}
|
||||
onChange={setName}
|
||||
required
|
||||
className="mt-1 block w-full text-foreground"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="email" className="block text-sm font-medium text-foreground">Email</label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="your@email.com"
|
||||
value={email}
|
||||
onChange={setEmail}
|
||||
required
|
||||
className="mt-1 block w-full text-foreground"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="phone" className="block text-sm font-medium text-foreground">Phone</label>
|
||||
<Input
|
||||
id="phone"
|
||||
type="tel"
|
||||
placeholder="(___) ___-____"
|
||||
value={phone}
|
||||
onChange={setPhone}
|
||||
className="mt-1 block w-full text-foreground"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="message" className="block text-sm font-medium text-foreground">Message</label>
|
||||
<textarea
|
||||
id="message"
|
||||
rows={4}
|
||||
placeholder="Tell us about your roofing needs..."
|
||||
value={message}
|
||||
onChange={(e) => setMessage(e.target.value)}
|
||||
className="mt-1 block w-full p-3 border border-border rounded-md shadow-sm bg-background text-foreground focus:ring-primary-cta focus:border-primary-cta sm:text-sm"
|
||||
style={{ '--border': 'var(--accent)', '--background': 'var(--card)' } as any}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
className="w-full py-3 px-4 rounded-md shadow-sm text-base font-medium text-primary-cta-text bg-primary-cta hover:bg-primary-cta-hover focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-cta-hover"
|
||||
>
|
||||
Submit Request
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div id="footer" data-section="footer">
|
||||
<FooterBaseCard
|
||||
logoText="Results Roofing"
|
||||
@@ -292,19 +199,19 @@ export default function LandingPage() {
|
||||
{
|
||||
title: "Quick Links", items: [
|
||||
{
|
||||
label: "Services", onClick: () => scrollToSection("services")},
|
||||
label: "Services", href: "#services"},
|
||||
{
|
||||
label: "Process", onClick: () => scrollToSection("process")},
|
||||
label: "Process", href: "#process"},
|
||||
{
|
||||
label: "Reviews", onClick: () => scrollToSection("reviews")},
|
||||
label: "Reviews", href: "#reviews"},
|
||||
{
|
||||
label: "FAQ", onClick: () => scrollToSection("faq")},
|
||||
label: "FAQ", href: "#faq"},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Contact", items: [
|
||||
{
|
||||
label: "Get an Estimate", onClick: () => scrollToSection("quote")},
|
||||
label: "Get an Estimate", href: "#quote"},
|
||||
{
|
||||
label: "Call Us: (214) 555-0199", href: "tel:+12145550199"},
|
||||
{
|
||||
@@ -323,7 +230,7 @@ export default function LandingPage() {
|
||||
copyrightText="© 2024 | Results Roofing. All rights reserved."
|
||||
/>
|
||||
</div>
|
||||
</ReactLenis>
|
||||
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,74 +1,57 @@
|
||||
"use client";
|
||||
import React, { forwardRef } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useButtonClick } from "@/components/button/useButtonClick";
|
||||
|
||||
import { useRef, memo } from "react";
|
||||
import { useCharAnimation } from "../useCharAnimation";
|
||||
import { useButtonClick } from "../useButtonClick";
|
||||
import { cls } from "@/lib/utils";
|
||||
import "./BounceButton.css";
|
||||
|
||||
interface ButtonBounceEffectProps {
|
||||
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
text: string;
|
||||
onClick?: () => void;
|
||||
href?: string;
|
||||
className?: string;
|
||||
bgClassName?: string;
|
||||
textClassName?: string;
|
||||
disabled?: boolean;
|
||||
ariaLabel?: string;
|
||||
type?: "button" | "submit" | "reset";
|
||||
scrollToSection?: boolean;
|
||||
iconClassName?: string;
|
||||
newTab?: boolean;
|
||||
}
|
||||
|
||||
const ButtonBounceEffect = ({
|
||||
text,
|
||||
onClick,
|
||||
href,
|
||||
className = "",
|
||||
bgClassName = "",
|
||||
textClassName = "",
|
||||
disabled = false,
|
||||
ariaLabel,
|
||||
type = "button",
|
||||
scrollToSection,
|
||||
}: ButtonBounceEffectProps) => {
|
||||
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||
const handleClick = useButtonClick(href, onClick, scrollToSection);
|
||||
export const ButtonBounceEffect = forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
(
|
||||
{
|
||||
text,
|
||||
onClick,
|
||||
href,
|
||||
className,
|
||||
bgClassName,
|
||||
textClassName,
|
||||
iconClassName,
|
||||
disabled = false,
|
||||
ariaLabel,
|
||||
type = "button", newTab,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const clickHandler = useButtonClick(href, onClick, newTab);
|
||||
|
||||
useCharAnimation(buttonRef, text);
|
||||
|
||||
return (
|
||||
<button
|
||||
ref={buttonRef}
|
||||
type={type}
|
||||
onClick={handleClick}
|
||||
data-href={href}
|
||||
disabled={disabled}
|
||||
aria-label={ariaLabel || text}
|
||||
className={cls(
|
||||
"bounce-button relative cursor-pointer flex items-center justify-center bg-transparent border-none leading-none no-underline h-9 px-6 min-w-0 w-fit max-w-full rounded-theme text-primary-cta-text",
|
||||
"disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cls(
|
||||
"bounce-button-bg absolute! inset-0 rounded-theme primary-button",
|
||||
bgClassName
|
||||
)}
|
||||
></div>
|
||||
<span
|
||||
data-button-animate-chars=""
|
||||
className={cls(
|
||||
"bounce-button-text relative text-sm inline-block overflow-hidden truncate whitespace-nowrap",
|
||||
textClassName
|
||||
return (
|
||||
<button
|
||||
className={cn(
|
||||
"group relative flex h-12 w-full items-center justify-center rounded-lg bg-primary-cta p-3 text-sm font-medium text-primary-cta-foreground transition-all duration-300 ease-out active:scale-95", className
|
||||
)}
|
||||
onClick={clickHandler}
|
||||
aria-label={ariaLabel}
|
||||
disabled={disabled}
|
||||
type={type}
|
||||
ref={ref}
|
||||
{...props}
|
||||
>
|
||||
{text}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
<span
|
||||
className={cn(
|
||||
"relative flex items-center gap-2 translate-y-0 group-hover:-translate-y-1 group-active:translate-y-0 transition-transform duration-300 ease-out", textClassName
|
||||
)}
|
||||
>
|
||||
{text}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
ButtonBounceEffect.displayName = "ButtonBounceEffect";
|
||||
|
||||
export default memo(ButtonBounceEffect);
|
||||
|
||||
@@ -1,74 +1,33 @@
|
||||
"use client";
|
||||
|
||||
import { useLenis } from "lenis/react";
|
||||
import { useRouter, usePathname } from "next/navigation";
|
||||
import { useEffect } from "react";
|
||||
import { useCallback } from "react";
|
||||
// No import for 'lenis' or 'lenis/react'
|
||||
|
||||
export const useButtonClick = (
|
||||
href?: string,
|
||||
onClick?: () => void,
|
||||
scrollToSection?: boolean
|
||||
) => {
|
||||
const lenis = useLenis();
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
/**
|
||||
* A hook to provide a consistent click handler for buttons, especially for internal hash navigation.
|
||||
* It ensures smooth scrolling without relying on external libraries like Lenis or problematic querySelector patterns.
|
||||
*/
|
||||
export function useButtonClick() {
|
||||
const handleButtonClick = useCallback((event: React.MouseEvent<HTMLButtonElement | HTMLAnchorElement>, href?: string) => {
|
||||
if (href && href.startsWith("#")) {
|
||||
const id = href.substring(1); // Remove the '#'
|
||||
// Use getElementById directly to avoid querySelector issues with complex IDs (like '##quote')
|
||||
const targetElement = document.getElementById(id);
|
||||
|
||||
const scrollToElement = (sectionId: string, delay: number = 100) => {
|
||||
setTimeout(() => {
|
||||
if (lenis) {
|
||||
lenis.scrollTo(`#${sectionId}`, { offset: 0 });
|
||||
if (targetElement) {
|
||||
event.preventDefault(); // Prevent default browser jump
|
||||
targetElement.scrollIntoView({ behavior: "smooth" });
|
||||
// Optionally update the URL hash without a full page reload or additional scroll
|
||||
// history.pushState(null, '', href);
|
||||
} else {
|
||||
const element = document.getElementById(sectionId);
|
||||
if (element) {
|
||||
element.scrollIntoView({ behavior: "smooth", block: "start" });
|
||||
}
|
||||
}
|
||||
}, delay);
|
||||
};
|
||||
|
||||
const handleClick = () => {
|
||||
if (href) {
|
||||
const isExternalLink = /^(https?:\/\/|www\.)/.test(href);
|
||||
|
||||
if (isExternalLink) {
|
||||
window.open(
|
||||
href.startsWith("www.") ? `https://${href}` : href,
|
||||
"_blank",
|
||||
"noopener,noreferrer"
|
||||
);
|
||||
} else if (href.startsWith("/")) {
|
||||
const [path, hash] = href.split("#");
|
||||
|
||||
if (path !== pathname) {
|
||||
router.push(path);
|
||||
if (hash) {
|
||||
setTimeout(() => {
|
||||
window.location.hash = hash;
|
||||
scrollToElement(hash, 100);
|
||||
}, 100);
|
||||
}
|
||||
} else {
|
||||
if (hash) {
|
||||
window.location.hash = hash;
|
||||
scrollToElement(hash, 50);
|
||||
} else if (scrollToSection) {
|
||||
const sectionId = path.replace(/^\//, "").replace(/\//g, "-");
|
||||
scrollToElement(sectionId, 50);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
scrollToElement(href, 50);
|
||||
console.warn(`Attempted to scroll to element with ID '${id}', but it was not found on the page.`);
|
||||
// If the element is not found, we let the default behavior happen (which might lead to a page refresh to the root)
|
||||
// or prevent it and do nothing. Given it's a "fix" for a bug, a silent failure with a console warning is safer than unexpected navigation.
|
||||
}
|
||||
}
|
||||
onClick?.();
|
||||
};
|
||||
// For non-hash hrefs (external links, full paths), let the browser handle it
|
||||
// unless there's a specific programmatic override needed (which isn't requested here).
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window !== "undefined" && window.location.hash) {
|
||||
const hash = window.location.hash.replace("#", "");
|
||||
scrollToElement(hash, 300);
|
||||
}
|
||||
}, [pathname]);
|
||||
|
||||
return handleClick;
|
||||
};
|
||||
return { handleButtonClick };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user