Fazekit

Code

"use client";
import * as React from "react";
import { MotionConfig, useReducedMotion } from "motion/react";
import { cn } from "@/lib/utils";
import { defaultHandymanData, type HandymanData } from "./data";
import { Contact, Faq, Footer, Reviews, type ContactPrefill, type JobRequest } from "./sections/closing";
import { Hero, Process, Stats } from "./sections/hero";
import { EmergencyBanner, MobileCallBar, Navbar } from "./sections/navbar";
import { jobForService, QuoteEstimator, Services, type QuoteSelection } from "./sections/services";
import { brandProps, scrollToId, useMoney, useOpenStatus } from "./sections/ui";
import { AreaChecker, BeforeAfterSection, Certifications } from "./sections/work";

export type { HandymanData, JobRequest, QuoteSelection };
export { defaultHandymanData };

export interface HandymanServicesTemplateProps {
  /** Override any top-level content group (brand, services, quote prices, areas…). Missing groups fall back to the demo content in data.ts. */
  data?: Partial<HandymanData>;
  /** Called when a visitor sends the job request form. Return a promise to keep the loading state until your API answers. */
  onSubmitJob?: (request: JobRequest) => void | Promise<void>;
  className?: string;
}

/**
 * FixRight — a complete website for a local plumber / electrician / handyman.
 * Emergency call-out banner, hero with live dispatch card, services with "from" prices, instant quote estimator,
 * postcode service-area checker with map, before/after slider, certifications, reviews, FAQ,
 * job request form with validation, opening hours with live "open now", map + directions, and a legal footer.
 */
export function HandymanServicesTemplate({ data, onSubmitJob, className }: HandymanServicesTemplateProps) {
  const d: HandymanData = React.useMemo(() => ({ ...defaultHandymanData, ...data }), [data]);
  const reduce = useReducedMotion();
  const money = useMoney(d.brand.currency, d.brand.locale);
  const status = useOpenStatus(d.hours);
  const brand = brandProps(d.brandColors);
  const [jobId, setJobId] = React.useState(d.quote.jobs[0].id);
  const [prefill, setPrefill] = React.useState<ContactPrefill>({ nonce: 0 });

  const go = React.useCallback((id: string) => scrollToId(id, reduce), [reduce]);

  const quoteFor = (serviceId: string) => {
    setJobId(jobForService(serviceId, d));
    go("quote");
  };

  const book = (q: QuoteSelection) => {
    const job = d.quote.jobs.find((j) => j.id === q.jobId);
    const size = d.quote.sizes.find((s) => s.id === q.size);
    const urgency = d.quote.urgencies.find((u) => u.id === q.urgencyId);
    setPrefill((p) => ({
      nonce: p.nonce + 1,
      jobType: job?.label,
      details: `Estimate: ${job?.label} · ${size?.label} job · ${urgency?.label} (${money.format(q.low)}–${money.format(q.high)}).\n`,
    }));
    go("contact");
  };

  return (
    <MotionConfig reducedMotion="user">
      <div className={cn("relative w-full bg-background font-sans text-foreground antialiased selection:bg-primary/25", brand.className, className)} style={brand.style}>
        <EmergencyBanner data={d} />
        <Navbar data={d} status={status} onQuote={() => go("quote")} />
        <main>
          <Hero data={d} onQuote={() => go("quote")} />
          <Stats data={d} />
          <Services services={d.services} money={money} onQuote={quoteFor} />
          <QuoteEstimator data={d} money={money} jobId={jobId} onJobChange={setJobId} onBook={book} />
          <AreaChecker data={d} />
          <Process data={d} />
          <BeforeAfterSection projects={d.projects} />
          <Certifications data={d} />
          <Reviews data={d} />
          <Faq data={d} />
          <Contact data={d} status={status} prefill={prefill} onSubmit={onSubmitJob} />
        </main>
        <Footer data={d} onNav={go} />
        <MobileCallBar data={d} onQuote={() => go("quote")} />
      </div>
    </MotionConfig>
  );
}

More in Landing Pages

View all →