Fazekit

Code

"use client";
import * as React from "react";
import { MotionConfig, useReducedMotion } from "motion/react";
import { cn } from "@/lib/utils";
import { defaultTravelData, type TravelData } from "./data";
import { Contact, Faq, Footer, Newsletter, Reviews, type EnquiryPrefill, type TravelEnquiry } from "./sections/closing";
import { Hero, Navbar, Trust, type SearchQuery } from "./sections/hero";
import { Deals, TripBuilder, type BuilderResult } from "./sections/planner";
import { Approach, Trips } from "./sections/trips";
import { brandProps, scrollToId, useMoney, useOpenStatus } from "./sections/ui";

export type { TravelData, TravelEnquiry, SearchQuery, BuilderResult };
export { defaultTravelData };

export interface TravelAgencyTemplateProps {
  /** Override any top-level content group (brand, trips, deals, reviews…). Missing groups fall back to data.ts. */
  data?: Partial<TravelData>;
  /** Called when the enquiry form is sent. Return a promise to keep the loading state until your API answers. */
  onEnquire?: (enquiry: TravelEnquiry) => void | Promise<void>;
  /** Called when someone subscribes to the newsletter. */
  onSubscribe?: (email: string) => void | Promise<void>;
  className?: string;
}

/**
 * Wayfarer — an editorial website for a boutique travel agency.
 * Parallax SVG landscape hero with destination search (where / when / budget), trust stats, featured journeys
 * with illustrated art and a day-by-day itinerary dialog, a 4-step trip builder with price estimate, a live
 * countdown for weekly offers, reviews, FAQ, newsletter, enquiry form, office hours ("open now"), map + directions.
 */
export function TravelAgencyTemplate({ data, onEnquire, onSubscribe, className }: TravelAgencyTemplateProps) {
  const d: TravelData = React.useMemo(() => ({ ...defaultTravelData, ...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 [query, setQuery] = React.useState<SearchQuery | null>(null);
  const [prefill, setPrefill] = React.useState<EnquiryPrefill>({ nonce: 0 });
  const go = React.useCallback((id: string) => scrollToId(id, reduce), [reduce]);
  const enquire = (p: Omit<EnquiryPrefill, "nonce">) => {
    setPrefill((c) => ({ ...p, nonce: c.nonce + 1 }));
    go("contact");
  };

  return (
    <MotionConfig reducedMotion="user">
      <div className={cn("relative w-full bg-background text-foreground antialiased selection:bg-primary/20", brand.className, className)} style={brand.style}>
        <Navbar data={d} status={status} onPlan={() => go("builder")} />
        <main>
          <Hero
            data={d}
            money={money}
            onSearch={(q) => {
              setQuery(q);
              go("trips");
            }}
          />
          <Trust data={d} />
          <Trips data={d} money={money} query={query} onClear={() => setQuery(null)} onEnquire={(t) => enquire({ trip: t.name, message: `I'd love to know more about ${t.name} (${t.nights} nights).` })} />
          <Approach data={d} />
          <TripBuilder
            data={d}
            money={money}
            onSend={(r: BuilderResult) =>
              enquire({
                trip: r.trip.name,
                travellers: r.answers.travellers,
                message: `Trip builder: ${r.answers.styles.join(", ")} · ${r.answers.nights} nights · ${r.answers.pace} pace · budget up to ${money.format(r.answers.budget)} pp. Estimate ${money.format(r.low)}–${money.format(r.high)} pp.`,
              })
            }
          />
          <Deals data={d} money={money} onEnquire={(t, label) => enquire({ trip: t.name, message: `Please hold a place on the "${label}" offer for ${t.name}.` })} />
          <Reviews data={d} />
          <Faq data={d} />
          <Newsletter onSubscribe={onSubscribe} />
          <Contact data={d} status={status} prefill={prefill} onSubmit={onEnquire} />
        </main>
        <Footer data={d} onNav={go} />
      </div>
    </MotionConfig>
  );
}

More in Landing Pages

View all →